Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / View Controllers / OTRMessagesViewController.m @ 3322c5c9

History | View | Annotate | Download (97.3 KB)

1
//
2
//  OTRMessagesViewController.m
3
//  Off the Record
4
//
5
//  Created by David Chiles on 5/12/14.
6
//  Copyright (c) 2014 Chris Ballinger. All rights reserved.
7
//
8

    
9
#import "OTRMessagesViewController.h"
10

    
11
#import "OTRDatabaseView.h"
12
#import "OTRDatabaseManager.h"
13
#import "OTRLog.h"
14

    
15
#import "OTRBuddy.h"
16
#import "OTRAccount.h"
17
#import "OTRMessage+JSQMessageData.h"
18
@import JSQMessagesViewController;
19
@import MobileCoreServices;
20
#import "OTRProtocolManager.h"
21
#import "OTRXMPPTorAccount.h"
22
#import "OTRXMPPManager.h"
23
#import "OTRLockButton.h"
24
#import "OTRButtonView.h"
25
@import OTRAssets;
26
#import "OTRTitleSubtitleView.h"
27
@import OTRKit;
28
@import FormatterKit;
29
#import "OTRImages.h"
30
#import "UIActivityViewController+ChatSecure.h"
31
#import "OTRUtilities.h"
32
#import "OTRProtocolManager.h"
33
#import "OTRColors.h"
34
#import "JSQMessagesCollectionViewCell+ChatSecure.h"
35
@import BButton;
36
#import "OTRAttachmentPicker.h"
37
#import "OTRImageItem.h"
38
#import "OTRVideoItem.h"
39
#import "OTRAudioItem.h"
40
@import JTSImageViewController;
41
#import "OTRAudioControlsView.h"
42
#import "OTRPlayPauseProgressView.h"
43
#import "OTRAudioPlaybackController.h"
44
#import "OTRMediaFileManager.h"
45
#import "OTRMediaServer.h"
46
#import "UIImage+ChatSecure.h"
47
#import "OTRBaseLoginViewController.h"
48

    
49
#import <ChatSecureCore/ChatSecureCore-Swift.h>
50
#import "OTRYapMessageSendAction.h"
51
#import "UIViewController+ChatSecure.h"
52
#import "OTRBuddyCache.h"
53
#import "OTRTextItem.h"
54
#import "OTRHTMLItem.h"
55
#import "OTRFileItem.h"
56
@import YapDatabase;
57
@import PureLayout;
58
@import KVOController;
59

    
60
@import AVFoundation;
61
@import MediaPlayer;
62

    
63
static NSTimeInterval const kOTRMessageSentDateShowTimeInterval = 5 * 60;
64
static NSUInteger const kOTRMessagePageSize = 50;
65

    
66
typedef NS_ENUM(int, OTRDropDownType) {
67
    OTRDropDownTypeNone          = 0,
68
    OTRDropDownTypeEncryption    = 1,
69
    OTRDropDownTypePush          = 2
70
};
71

    
72
@interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPickerDelegate, OTRYapViewHandlerDelegateProtocol, OTRMessagesCollectionViewFlowLayoutSizeProtocol, OTRRoomOccupantsViewControllerDelegate> {
73
    JSQMessagesAvatarImage *_warningAvatarImage;
74
    JSQMessagesAvatarImage *_accountAvatarImage;
75
    JSQMessagesAvatarImage *_buddyAvatarImage;
76
}
77

    
78
@property (nonatomic, strong) OTRYapViewHandler *viewHandler;
79

    
80
@property (nonatomic, strong) JSQMessagesBubbleImage *outgoingBubbleImage;
81
@property (nonatomic, strong) JSQMessagesBubbleImage *incomingBubbleImage;
82

    
83
@property (nonatomic, weak) id didFinishGeneratingPrivateKeyNotificationObject;
84
@property (nonatomic, weak) id messageStateDidChangeNotificationObject;
85
@property (nonatomic, weak) id pendingApprovalDidChangeNotificationObject;
86
@property (nonatomic, weak) id deviceListUpdateNotificationObject;
87

    
88

    
89
@property (nonatomic ,strong) UIBarButtonItem *lockBarButtonItem;
90
@property (nonatomic, strong) OTRLockButton *lockButton;
91
@property (nonatomic, strong) OTRButtonView *buttonDropdownView;
92

    
93
@property (nonatomic, strong) OTRAttachmentPicker *attachmentPicker;
94
@property (nonatomic, strong) OTRAudioPlaybackController *audioPlaybackController;
95

    
96
@property (nonatomic, strong) NSTimer *lastSeenRefreshTimer;
97
@property (nonatomic, strong) UIView *jidForwardingHeaderView;
98

    
99
@property (nonatomic) BOOL loadingMessages;
100
@property (nonatomic) BOOL messageRangeExtended;
101
@property (nonatomic, strong) NSIndexPath *currentIndexPath;
102
@property (nonatomic, strong) id currentMessage;
103
@property (nonatomic, strong) NSCache *messageSizeCache;
104

    
105
@end
106

    
107
@implementation OTRMessagesViewController
108

    
109
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
110
{
111
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
112
        self.senderId = @"";
113
        self.senderDisplayName = @"";
114
        _state = [[MessagesViewControllerState alloc] init];
115
        self.messageSizeCache = [NSCache new];
116
        self.messageSizeCache.countLimit = kOTRMessagePageSize;
117
        self.messageRangeExtended = NO;
118
    }
119
    return self;
120
}
121

    
122
#pragma - mark Lifecylce Methods
123

    
124
- (void) dealloc {
125
    [self.lastSeenRefreshTimer invalidate];
126
    [[NSNotificationCenter defaultCenter] removeObserver:self];
127
}
128

    
129
- (void)viewDidLoad
130
{
131
    [super viewDidLoad];
132
    
133
    self.automaticallyScrollsToMostRecentMessage = YES;
134
    
135
     ////// bubbles //////
136
    JSQMessagesBubbleImageFactory *bubbleImageFactory = [[JSQMessagesBubbleImageFactory alloc] init];
137
                                                         
138
    self.outgoingBubbleImage = [bubbleImageFactory outgoingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleBlueColor]];
139
    
140
    self.incomingBubbleImage = [bubbleImageFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]];
141
    
142
    ////// TitleView //////
143
    OTRTitleSubtitleView *titleView = [self titleView];
144
    [self refreshTitleView:titleView];
145
    self.navigationItem.titleView = titleView;
146
    
147
    ////// Send Button //////
148
    self.sendButton = [JSQMessagesToolbarButtonFactory defaultSendButtonItem];
149
    
150
    ////// Attachment Button //////
151
    self.inputToolbar.contentView.leftBarButtonItem = nil;
152
    self.cameraButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
153
    self.cameraButton.titleLabel.font = [UIFont fontWithName:kFontAwesomeFont size:20];
154
    self.cameraButton.titleLabel.textAlignment = NSTextAlignmentCenter;
155
    [self.cameraButton setTitle:[NSString fa_stringForFontAwesomeIcon:FACamera] forState:UIControlStateNormal];
156
    self.cameraButton.frame = CGRectMake(0, 0, 32, 32);
157
    
158
    ////// Microphone Button //////
159
    self.microphoneButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
160
    self.microphoneButton.frame = CGRectMake(0, 0, 32, 32);
161
    self.microphoneButton.titleLabel.font = [UIFont fontWithName:kFontAwesomeFont size:20];
162
    self.microphoneButton.titleLabel.textAlignment = NSTextAlignmentCenter;
163
    [self.microphoneButton setTitle:[NSString fa_stringForFontAwesomeIcon:FAMicrophone]
164
          forState:UIControlStateNormal];
165
    
166
    self.audioPlaybackController = [[OTRAudioPlaybackController alloc] init];
167
    
168
    ////// TextViewUpdates //////
169
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedTextViewChangedNotification:) name:UITextViewTextDidChangeNotification object:self.inputToolbar.contentView.textView];
170
    
171
    /** Setup databse view handler*/
172
    self.viewHandler = [[OTRYapViewHandler alloc] initWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection databaseChangeNotificationName:[DatabaseNotificationName LongLivedTransactionChanges]];
173
    self.viewHandler.delegate = self;
174
    
175
    ///Custom Layout to account for no bubble cells
176
    OTRMessagesCollectionViewFlowLayout *layout = [[OTRMessagesCollectionViewFlowLayout alloc] init];
177
    layout.sizeDelegate = self;
178
    self.collectionView.collectionViewLayout = layout;
179

    
180
    ///"Loading Earlier" header view
181
    [self.collectionView registerNib:[UINib nibWithNibName:@"OTRMessagesLoadingView" bundle:OTRAssets.resourcesBundle]
182
          forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
183
                 withReuseIdentifier:[JSQMessagesLoadEarlierHeaderView headerReuseIdentifier]];
184

    
185
    //Subscribe to changes in encryption state
186
    __weak typeof(self)weakSelf = self;
187
    [self.KVOController observe:self.state keyPath:NSStringFromSelector(@selector(messageSecurity)) options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
188
        __typeof__(self) strongSelf = weakSelf;
189
        if (!strongSelf) { return; }
190
        
191
        if ([object isKindOfClass:[MessagesViewControllerState class]]) {
192
            MessagesViewControllerState *state = (MessagesViewControllerState*)object;
193
            NSString * placeHolderString = nil;
194
            switch (state.messageSecurity) {
195
                case OTRMessageTransportSecurityPlaintext:
196
                case OTRMessageTransportSecurityPlaintextWithOTR:
197
                    placeHolderString = SEND_PLAINTEXT_STRING();
198
                    break;
199
                case OTRMessageTransportSecurityOTR:
200
                    placeHolderString = [NSString stringWithFormat:SEND_ENCRYPTED_STRING(),@"OTR"];
201
                    break;
202
                case OTRMessageTransportSecurityOMEMO:
203
                    placeHolderString = [NSString stringWithFormat:SEND_ENCRYPTED_STRING(),@"OMEMO"];;
204
                    break;
205
                    
206
                default:
207
                    placeHolderString = [NSBundle jsq_localizedStringForKey:@"new_message"];
208
                    break;
209
            }
210
            strongSelf.inputToolbar.contentView.textView.placeHolder = placeHolderString;
211
            [self didUpdateState];
212
        }
213
    }];
214
    
215
}
216

    
217
- (void)viewDidAppear:(BOOL)animated
218
{
219
    [super viewDidAppear:animated];
220
    [self tryToMarkAllMessagesAsRead];
221
    // This is a hack to attempt fixing https://github.com/ChatSecure/ChatSecure-iOS/issues/657
222
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
223
        [self scrollToBottomAnimated:animated];
224
    });
225
    self.loadingMessages = NO;
226
    [self fetchMessageHistory];
227
}
228

    
229
- (void)viewWillAppear:(BOOL)animated
230
{
231
    self.currentIndexPath = nil;
232
    
233
    [super viewWillAppear:animated];
234
    [[UIApplication sharedApplication] setStatusBarHidden:NO];
235
    
236
    if (self.lastSeenRefreshTimer) {
237
        [self.lastSeenRefreshTimer invalidate];
238
        _lastSeenRefreshTimer = nil;
239
    }
240
    _lastSeenRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(refreshTitleTimerUpdate:) userInfo:nil repeats:YES];
241
    
242
    __weak typeof(self)weakSelf = self;
243
    void (^refreshGeneratingLock)(OTRAccount *) = ^void(OTRAccount * account) {
244
        __strong typeof(weakSelf)strongSelf = weakSelf;
245
        __block NSString *accountKey = nil;
246
        [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
247
            accountKey = [strongSelf buddyWithTransaction:transaction].accountUniqueId;
248
        }];
249
        if ([account.uniqueId isEqualToString:accountKey]) {
250
            [strongSelf updateEncryptionState];
251
        }
252
        
253
        
254
    };
255
    
256
    self.didFinishGeneratingPrivateKeyNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRDidFinishGeneratingPrivateKeyNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
257
        if ([note.object isKindOfClass:[OTRAccount class]]) {
258
            refreshGeneratingLock(note.object);
259
        }
260
    }];
261
   
262
    self.messageStateDidChangeNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRMessageStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
263
        __strong typeof(weakSelf)strongSelf = weakSelf;
264
        if ([note.object isKindOfClass:[OTRBuddy class]]) {
265
            OTRBuddy *notificationBuddy = note.object;
266
            __block NSString *buddyKey = nil;
267
            [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
268
                buddyKey = [strongSelf buddyWithTransaction:transaction].uniqueId;
269
            }];
270
            if ([notificationBuddy.uniqueId isEqualToString:buddyKey]) {
271
                [strongSelf updateEncryptionState];
272
            }
273
        }
274
    }];
275
    
276
    if ([self.threadKey length]) {
277
        [self.viewHandler.keyCollectionObserver observe:self.threadKey collection:self.threadCollection];
278
        [self updateViewWithKey:self.threadKey collection:self.threadCollection];
279
        [self.viewHandler setup:OTRFilteredChatDatabaseViewExtensionName groups:@[self.threadKey]];
280
        if(![self.inputToolbar.contentView.textView.text length]) {
281
            [self moveLastComposingTextForThreadKey:self.threadKey colleciton:self.threadCollection toTextView:self.inputToolbar.contentView.textView];
282
        }
283
    }
284

    
285
    self.loadingMessages = YES;
286
    [self.collectionView reloadData];
287
}
288

    
289
- (void)viewWillDisappear:(BOOL)animated
290
{
291
    [super viewWillDisappear:animated];
292
    
293
    [self.lastSeenRefreshTimer invalidate];
294
    self.lastSeenRefreshTimer = nil;
295
    
296
    [self saveCurrentMessageText:self.inputToolbar.contentView.textView.text threadKey:self.threadKey colleciton:self.threadCollection];
297
    
298
    [[NSNotificationCenter defaultCenter] removeObserver:self.messageStateDidChangeNotificationObject];
299
    [[NSNotificationCenter defaultCenter] removeObserver:self.didFinishGeneratingPrivateKeyNotificationObject];
300
    
301
    // [self.inputToolbar.contentView.textView resignFirstResponder];
302
}
303

    
304
- (void)viewDidDisappear:(BOOL)animated
305
{
306
    [super viewDidDisappear:animated];
307

    
308
    _warningAvatarImage = nil;
309
    _accountAvatarImage = nil;
310
    _buddyAvatarImage = nil;
311
}
312

    
313
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
314
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
315
    
316
    // After the transition is done, we need to reset the size caches and relayout
317
    // Do this using the technique in https://stackoverflow.com/questions/26943808/ios-how-to-run-a-function-after-device-has-rotated-swift
318
    [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
319
        [self.messageSizeCache removeAllObjects];
320
        [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
321
    }];
322
}
323

    
324
#pragma - mark Setters & getters
325

    
326
- (void) fetchMessageHistory {
327
    [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
328
        id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction];
329
        OTRXMPPManager *xmpp = [self xmppManagerWithTransaction:transaction];
330
        [xmpp fetchHistoryForThread:thread transaction:transaction];
331
    }];
332
}
333

    
334
- (OTRAttachmentPicker *)attachmentPicker
335
{
336
    if (!_attachmentPicker) {
337
        _attachmentPicker = [[OTRAttachmentPicker alloc] initWithParentViewController:self delegate:self];
338
    }
339
    return _attachmentPicker;
340
}
341

    
342
- (NSArray*) indexPathsToCount:(NSUInteger)count {
343
    NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:count];
344
    for (NSUInteger i = 0; i < count; i++) {
345
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
346
        [indexPaths addObject:indexPath];
347
    }
348
    return indexPaths;
349
}
350

    
351
- (nullable id<OTRThreadOwner>)threadObjectWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
352
    if (!self.threadKey || !self.threadCollection || !transaction) { return nil; }
353
    id object = [transaction objectForKey:self.threadKey inCollection:self.threadCollection];
354
    if ([object conformsToProtocol:@protocol(OTRThreadOwner)]) {
355
        return object;
356
    }
357
    return nil;
358
}
359

    
360
- (nullable OTRBuddy *)buddyWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
361
    id <OTRThreadOwner> object = [self threadObjectWithTransaction:transaction];
362
    if ([object isKindOfClass:[OTRBuddy class]]) {
363
        return (OTRBuddy *)object;
364
    }
365
    return nil;
366
}
367

    
368
- (nullable OTRXMPPRoom *)roomWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
369
    id <OTRThreadOwner> object = [self threadObjectWithTransaction:transaction];
370
    if ([object isKindOfClass:[OTRXMPPRoom class]]) {
371
        return (OTRXMPPRoom *)object;
372
    }
373
    return nil;
374
}
375

    
376
- (nullable OTRAccount *)accountWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
377
    id <OTRThreadOwner> thread =  [self threadObjectWithTransaction:transaction];
378
    if (!thread) { return nil; }
379
    OTRAccount *account = [OTRAccount fetchObjectWithUniqueID:[thread threadAccountIdentifier] transaction:transaction];
380
    return account;
381
}
382

    
383
- (void)setThreadKey:(NSString *)key collection:(NSString *)collection
384
{
385
    self.currentIndexPath = nil;
386
    NSString *oldKey = self.threadKey;
387
    NSString *oldCollection = self.threadCollection;
388
    
389
    self.threadKey = key;
390
    self.threadCollection = collection;
391
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
392
        self.senderId = [[self threadObjectWithTransaction:transaction] threadAccountIdentifier];
393
    }];
394
    
395
    // Clear out old state (don't just alloc a new object, we have KVOs attached to this!)
396
    self.state.canSendMedia = NO;
397
    self.state.canKnock = NO;
398
    self.state.messageSecurity = OTRMessageTransportSecurityInvalid;
399
    self.state.hasText = NO;
400
    self.state.isThreadOnline = NO;
401
    self.showTypingIndicator = NO;
402
    
403
    // This is set to nil so the refreshTitleView: method knows to reset username instead of last seen time
404
    [self titleView].subtitleLabel.text = nil;
405
    
406
    if (![oldKey isEqualToString:key] || ![oldCollection isEqualToString:collection]) {
407
        [self saveCurrentMessageText:self.inputToolbar.contentView.textView.text threadKey:oldKey colleciton:oldCollection];
408
        self.inputToolbar.contentView.textView.text = nil;
409
        [self receivedTextViewChanged:self.inputToolbar.contentView.textView];
410
    }
411
    
412
    [self.viewHandler.keyCollectionObserver stopObserving:oldKey collection:oldCollection];
413
    if (self.threadKey && self.threadCollection) {
414
        [self.viewHandler.keyCollectionObserver observe:self.threadKey collection:self.threadCollection];
415
        [self updateViewWithKey:self.threadKey collection:self.threadCollection];
416
        [self.viewHandler setup:OTRFilteredChatDatabaseViewExtensionName groups:@[self.threadKey]];
417
        [self moveLastComposingTextForThreadKey:self.threadKey colleciton:self.threadCollection toTextView:self.inputToolbar.contentView.textView];
418
    } else {
419
        // Reset the view handler
420
        self.viewHandler = [[OTRYapViewHandler alloc] initWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection databaseChangeNotificationName:[DatabaseNotificationName LongLivedTransactionChanges]];
421
        self.viewHandler.delegate = self;
422
        self.senderDisplayName = @"";
423
        self.senderId = @"";
424
    }
425
    
426
    [self.collectionView reloadData];
427
    
428
    // Profile Info Button
429
    [self setupInfoButton];
430
    
431
    [self updateEncryptionState];
432
    [self updateJIDForwardingHeader];
433
    
434
    __weak typeof(self)weakSelf = self;
435
    if (self.pendingApprovalDidChangeNotificationObject == nil) {
436
        self.pendingApprovalDidChangeNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRBuddyPendingApprovalDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
437
            __strong typeof(weakSelf)strongSelf = weakSelf;
438
            OTRXMPPBuddy *notificationBuddy = [note.userInfo objectForKey:@"buddy"];
439
            __block NSString *buddyKey = nil;
440
            [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
441
                buddyKey = [strongSelf buddyWithTransaction:transaction].uniqueId;
442
            }];
443
            if ([notificationBuddy.uniqueId isEqualToString:buddyKey]) {
444
                [strongSelf fetchOMEMODeviceList];
445
                [strongSelf sendPresenceProbe];
446
            }
447
        }];
448
    }
449
    
450
    if (self.deviceListUpdateNotificationObject == nil) {
451
        self.deviceListUpdateNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTROMEMOSignalCoordinator.DeviceListUpdateNotificationName object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
452
            __strong typeof(weakSelf)strongSelf = weakSelf;
453
            XMPPJID *notificationJid = [note.userInfo objectForKey:@"jid"];
454
            __block NSString *buddyUser = nil;
455
            [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
456
                buddyUser = [strongSelf buddyWithTransaction:transaction].username;
457
            }];
458
            if (notificationJid != nil && [notificationJid.bare isEqualToString:buddyUser]) {
459
                [strongSelf updateEncryptionState];
460
            }
461
        }];
462
    }
463
    
464
    [self sendPresenceProbe];
465
    [self fetchOMEMODeviceList];
466
}
467

    
468
                           
469
- (YapDatabaseConnection *)readOnlyDatabaseConnection
470
{
471
    if (!_readOnlyDatabaseConnection) {
472
        _readOnlyDatabaseConnection = [OTRDatabaseManager sharedInstance].readOnlyDatabaseConnection;
473
    }
474
    return _readOnlyDatabaseConnection;
475
}
476
                           
477
- (YapDatabaseConnection *)readWriteDatabaseConnection
478
{
479
    if (!_readWriteDatabaseConnection) {
480
            _readWriteDatabaseConnection = [OTRDatabaseManager sharedInstance].readWriteDatabaseConnection;
481
    }
482
    return _readWriteDatabaseConnection;
483
}
484
                        
485

    
486
- (nullable OTRXMPPManager *)xmppManagerWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
487
    OTRAccount *account = [self accountWithTransaction:transaction];
488
    if (!account) { return nil; }
489
    return (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account];
490
}
491

    
492
/** Will send a probe to fetch last seen */
493
- (void) sendPresenceProbe {
494
    __block OTRXMPPManager *xmpp = nil;
495
    __block OTRXMPPBuddy *buddy = nil;
496
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
497
        xmpp = [self xmppManagerWithTransaction:transaction];
498
        buddy = (OTRXMPPBuddy*)[self buddyWithTransaction:transaction];
499
    }];
500
    if (!xmpp || ![buddy isKindOfClass:[OTRXMPPBuddy class]] || buddy.pendingApproval) { return; }
501
    [xmpp sendPresenceProbeForBuddy:buddy];
502
}
503

    
504
- (void)updateViewWithKey:(NSString *)key collection:(NSString *)collection
505
{
506
    if ([collection isEqualToString:[OTRBuddy collection]]) {
507
        __block OTRBuddy *buddy = nil;
508
        __block OTRAccount *account = nil;
509
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
510
            buddy = [OTRBuddy fetchObjectWithUniqueID:key transaction:transaction];
511
            account = [OTRAccount fetchObjectWithUniqueID:buddy.accountUniqueId transaction:transaction];
512
        }];
513
        
514
        //Update UI now
515
        if (buddy.chatState == OTRChatStateComposing || buddy.chatState == OTRChatStatePaused) {
516
            self.showTypingIndicator = YES;
517
        }
518
        else {
519
            self.showTypingIndicator = NO;
520
        }
521
        
522
        // Update Buddy Status
523
        BOOL previousState = self.state.isThreadOnline;
524
        self.state.isThreadOnline = buddy.status != OTRThreadStatusOffline;
525
        
526
        [self didUpdateState];
527
        
528
        //Update Buddy knock status
529
        //Async because this calls down to the database and iterates over a relation. Might slowdown the UI if on main thread
530
        __weak __typeof__(self) weakSelf = self;
531
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
532
            __typeof__(self) strongSelf = weakSelf;
533
            __block BOOL canKnock = [[[OTRProtocolManager sharedInstance].pushController pushStorage] numberOfTokensForBuddy:buddy.uniqueId createdByThisAccount:NO] > 0;
534
            dispatch_async(dispatch_get_main_queue(), ^{
535
                if (canKnock != strongSelf.state.canKnock) {
536
                    strongSelf.state.canKnock = canKnock;
537
                    [strongSelf didUpdateState];
538
                }
539
            });
540
            
541
        });
542
        
543
        [self refreshTitleView:[self titleView]];
544

    
545
        // Auto-inititate OTR when contact comes online
546
        if (!previousState && self.state.isThreadOnline) {
547
            [[OTRProtocolManager sharedInstance].encryptionManager maybeRefreshOTRSessionForBuddyKey:key collection:collection];
548
        }
549
    } else if ([collection isEqualToString:[OTRXMPPRoom collection]]) {
550
        __block OTRXMPPRoom *room = nil;
551
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
552
            room = [OTRXMPPRoom fetchObjectWithUniqueID:key transaction:transaction];
553
        }];
554
        self.state.isThreadOnline = room.currentStatus != OTRThreadStatusOffline;
555
        [self didUpdateState];
556
        [self refreshTitleView:[self titleView]];
557
    }
558
    [self tryToMarkAllMessagesAsRead];
559
}
560

    
561
- (void)tryToMarkAllMessagesAsRead {
562
    // Set all messages as read
563
    if ([self otr_isVisible]) {
564
        __weak __typeof__(self) weakSelf = self;
565
        __block id <OTRThreadOwner>threadOwner = nil;
566
        __block NSArray <id <OTRMessageProtocol>>* unreadMessages = nil;
567
        [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
568
            threadOwner = [weakSelf threadObjectWithTransaction:transaction];
569
            if (!threadOwner) { return; }
570
            unreadMessages = [transaction allUnreadMessagesForThread:threadOwner];
571
        } completionBlock:^{
572
            
573
            if ([unreadMessages count] == 0) {
574
                return;
575
            }
576
            
577
            //Mark as read
578
            
579
            NSMutableArray <id <OTRMessageProtocol>>*toBeSaved = [[NSMutableArray alloc] init];
580
            
581
            [unreadMessages enumerateObjectsUsingBlock:^(id<OTRMessageProtocol>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
582
                if ([obj isKindOfClass:[OTRIncomingMessage class]]) {
583
                    OTRIncomingMessage *message = [((OTRIncomingMessage *)obj) copy];
584
                    message.read = YES;
585
                    [toBeSaved addObject:message];
586
                } else if ([obj isKindOfClass:[OTRXMPPRoomMessage class]]) {
587
                    OTRXMPPRoomMessage *message = [((OTRXMPPRoomMessage *)obj) copy];
588
                    message.read = YES;
589
                    [toBeSaved addObject:message];
590
                }
591
            }];
592
            
593
            [weakSelf.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
594
                [toBeSaved enumerateObjectsUsingBlock:^(id<OTRMessageProtocol>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
595
                    [transaction setObject:obj forKey:[obj messageKey] inCollection:[obj messageCollection]];
596
                }];
597
                [transaction touchObjectForKey:[threadOwner threadIdentifier] inCollection:[threadOwner threadCollection]];
598
            }];
599
        }];
600
    }
601
}
602

    
603
- (OTRTitleSubtitleView * __nonnull)titleView {
604
    UIView *titleView = self.navigationItem.titleView;
605
    if ([titleView isKindOfClass:[OTRTitleSubtitleView class]]) {
606
        return  (OTRTitleSubtitleView*)titleView;
607
    }
608
    return [[OTRTitleSubtitleView alloc] initWithFrame:CGRectMake(0, 0, 200, 44)];
609
}
610

    
611
- (void)refreshTitleTimerUpdate:(NSTimer*)timer {
612
    [self refreshTitleView:[self titleView]];
613
}
614

    
615
/** Updates the title view with the current thread information on this view controller*/
616
- (void)refreshTitleView:(OTRTitleSubtitleView *)titleView
617
{
618
    __block id<OTRThreadOwner> thread = nil;
619
    __block OTRAccount *account = nil;
620
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
621
        thread = [self threadObjectWithTransaction:transaction];
622
        account =  [self accountWithTransaction:transaction];
623
    }];
624
    
625
    titleView.titleLabel.text = [thread threadName];
626
    
627
    UIImage *statusImage = nil;
628
    if ([thread isKindOfClass:[OTRBuddy class]]) {
629
        OTRBuddy *buddy = (OTRBuddy*)thread;
630
        UIColor *color = [buddy avatarBorderColor];
631
        if (color) { // only show online status
632
            statusImage = [OTRImages circleWithRadius:50
633
                                      lineWidth:0
634
                                      lineColor:nil
635
                                      fillColor:color];
636
        }
637
        
638
        dispatch_block_t refreshTimeBlock = ^{
639
            __block OTRBuddy *buddy = nil;
640
            [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
641
                buddy = (OTRBuddy*)[self threadObjectWithTransaction:transaction];
642
            }];
643
            if (![buddy isKindOfClass:[OTRBuddy class]]) {
644
                return;
645
            }
646
            NSDate *lastSeen = [OTRBuddyCache.shared lastSeenDateForBuddy:buddy];
647
            OTRThreadStatus status = [OTRBuddyCache.shared threadStatusForBuddy:buddy];
648
            if (!lastSeen) {
649
                titleView.subtitleLabel.text = buddy.username;
650
                return;
651
            }
652
            TTTTimeIntervalFormatter *tf = [[TTTTimeIntervalFormatter alloc] init];
653
            tf.presentTimeIntervalMargin = 60;
654
            tf.usesAbbreviatedCalendarUnits = YES;
655
            NSTimeInterval lastSeenInterval = [lastSeen timeIntervalSinceDate:[NSDate date]];
656
            NSString *labelString = nil;
657
            if (status == OTRThreadStatusAvailable) {
658
                labelString = buddy.username;
659
            } else {
660
                labelString = [NSString stringWithFormat:@"%@ %@", ACTIVE_STRING(), [tf stringForTimeInterval:lastSeenInterval]];
661
            }
662
            titleView.subtitleLabel.text = labelString;
663
        };
664
        
665
        // Set the username if nothing else is set.
666
        // This should be cleared out when buddy is changed
667
        if (!titleView.subtitleLabel.text) {
668
            titleView.subtitleLabel.text = buddy.username;
669
        }
670
        
671
        // Show an "Last seen 11 min ago" in title bar after brief delay
672
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
673
            refreshTimeBlock();
674
        });
675
    } else if ([thread isGroupThread]) {
676
        titleView.subtitleLabel.text = GROUP_CHAT_STRING();
677
    } else {
678
        titleView.subtitleLabel.text = nil;
679
    }
680
    
681
    titleView.titleImageView.image = statusImage;
682

    
683
}
684

    
685
/**
686
 This generates a UIAlertAction where the handler fetches the outgoing message (optionaly duplicates). Then if media message resend media message. If not update messageSecurityInfo and date and create new sending action.
687
 */
688
- (UIAlertAction *)resendOutgoingMessageActionForMessageKey:(NSString *)messageKey
689
                                          messageCollection:(NSString *)messageCollection
690
                                readWriteDatabaseConnection:(YapDatabaseConnection*)databaseConnection
691
                                                      title:(NSString *)title
692
{
693
    UIAlertAction *action = [UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
694
        [databaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
695
            id object = [[transaction objectForKey:messageKey inCollection:messageCollection] copy];
696
            id<OTRMessageProtocol> message = nil;
697
            if ([object conformsToProtocol:@protocol(OTRMessageProtocol)]) {
698
                message = (id<OTRMessageProtocol>)object;
699
            } else {
700
                return;
701
            }
702
            // Messages that never sent properly don't need to be duplicated client-side
703
            NSError *messageError = message.messageError;
704
            message = [message duplicateMessage];
705
            message.messageError = nil;
706
            message.messageSecurity = self.state.messageSecurity;
707
            message.messageDate = [NSDate date];
708
            [message saveWithTransaction:transaction];
709
            
710
            // We only need to re-upload failed media messages
711
            // otherwise just resend the URL directly
712
            if (message.messageMediaItemKey.length &&
713
                (!message.messageText.length || messageError)) {
714
                OTRMediaItem *mediaItem = [OTRMediaItem fetchObjectWithUniqueID:message.messageMediaItemKey transaction:transaction];
715
                [self sendMediaItem:mediaItem data:nil message:message transaction:transaction];
716
            } else {
717
                OTRYapMessageSendAction *sendingAction = [OTRYapMessageSendAction sendActionForMessage:message date:message.messageDate];
718
                [sendingAction saveWithTransaction:transaction];
719
            }
720
        }];
721
    }];
722
    return action;
723
}
724

    
725
- (nonnull UIAlertAction *)viewProfileAction {
726
    return [UIAlertAction actionWithTitle:VIEW_PROFILE_STRING() style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
727
        [self infoButtonPressed:action];
728
    }];
729
}
730

    
731
- (nonnull UIAlertAction *)cancleAction {
732
    return [UIAlertAction actionWithTitle:CANCEL_STRING()
733
                                    style:UIAlertActionStyleCancel
734
                                  handler:nil];
735
}
736

    
737
- (NSArray <UIAlertAction *>*)actionForMessage:(id<OTRMessageProtocol>)message {
738
    NSMutableArray <UIAlertAction *>*actions = [[NSMutableArray alloc] init];
739
    
740
    if (!message.isMessageIncoming) {
741
        // This is an outgoing message so we can offer to resend
742
        UIAlertAction *resendAction = [self resendOutgoingMessageActionForMessageKey:message.messageKey messageCollection:message.messageCollection readWriteDatabaseConnection:self.readWriteDatabaseConnection  title:RESEND_STRING()];
743
        [actions addObject:resendAction];
744
    }
745
    
746
    if (![message isKindOfClass:[OTRXMPPRoomMessage class]]) {
747
        [actions addObject:[self viewProfileAction]];
748
    }
749
    
750
    NSArray<UIAlertAction*> *mediaActions = [UIAlertAction actionsForMediaMessage:message sourceView:self.view viewController:self];
751
    [actions addObjectsFromArray:mediaActions];
752
    
753
    [actions addObject:[self cancleAction]];
754
    return actions;
755
}
756

    
757
- (void)didTapAvatar:(id<OTRMessageProtocol>)message sender:(id)sender {
758
    NSError *error =  [message messageError];
759
    NSString *title = nil;
760
    NSString *alertMessage = nil;
761
    
762
    NSString * sendingType = UNENCRYPTED_STRING();
763
    switch (self.state.messageSecurity) {
764
        case OTRMessageTransportSecurityOTR:
765
            sendingType = @"OTR";
766
            break;
767
        case OTRMessageTransportSecurityOMEMO:
768
            sendingType = @"OMEMO";
769
            break;
770
            
771
        default:
772
            break;
773
    }
774
    
775
    if ([message isKindOfClass:[OTROutgoingMessage class]]) {
776
        title = RESEND_MESSAGE_TITLE();
777
        alertMessage = [NSString stringWithFormat:RESEND_DESCRIPTION_STRING(),sendingType];
778
    }
779
    
780
    if (error) {
781
        NSUInteger otrFingerprintError = 32872;
782
        title = ERROR_STRING();
783
        alertMessage = error.localizedDescription;
784
        
785
        if (error.code == otrFingerprintError) {
786
            alertMessage = NO_DEVICES_BUDDY_ERROR_STRING();
787
        }
788
        
789
        if([message isKindOfClass:[OTROutgoingMessage class]]) {
790
            //If it's an outgoing message the error title should be that we were unable to send the message.
791
            title = UNABLE_TO_SEND_STRING();
792
            
793
            
794
            
795
            NSString *resendDescription = [NSString stringWithFormat:RESEND_DESCRIPTION_STRING(),sendingType];
796
            alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",resendDescription]];
797
            
798
            //If this is an error about not having a trusted identity then we should offer to connect to the
799
            if (error.code == OTROMEMOErrorNoDevicesForBuddy ||
800
                error.code == OTROMEMOErrorNoDevices ||
801
                error.code == otrFingerprintError) {
802
                
803
                alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",VIEW_PROFILE_DESCRIPTION_STRING()]];
804
            }
805
        }
806
    }
807
    
808
    
809
    if (![self isMessageTrusted:message]) {
810
        title = UNTRUSTED_DEVICE_STRING();
811
        if ([message isMessageIncoming]) {
812
            alertMessage = UNTRUSTED_DEVICE_REVEIVED_STRING();
813
        } else {
814
            alertMessage = UNTRUSTED_DEVICE_SENT_STRING();
815
        }
816
        alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",VIEW_PROFILE_DESCRIPTION_STRING()]];
817
    }
818
    
819
    NSArray <UIAlertAction*>*actions = [self actionForMessage:message];
820
    if ([actions count] > 0) {
821
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:alertMessage preferredStyle:UIAlertControllerStyleActionSheet];
822
        [actions enumerateObjectsUsingBlock:^(UIAlertAction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
823
            [alertController addAction:obj];
824
        }];
825
        if ([sender isKindOfClass:[UIView class]]) {
826
            UIView *sourceView = sender;
827
            alertController.popoverPresentationController.sourceView = sourceView;
828
            alertController.popoverPresentationController.sourceRect = sourceView.bounds;
829
        }
830
        [self presentViewController:alertController animated:YES completion:nil];
831
    }
832
}
833

    
834
- (BOOL)isMessageTrusted:(id <OTRMessageProtocol>)message {
835
    BOOL trusted = YES;
836
    if (![message isKindOfClass:[OTRBaseMessage class]]) {
837
        return trusted;
838
    }
839
    
840
    OTRBaseMessage *baseMessage = (OTRBaseMessage *)message;
841
    
842
    
843
    if (baseMessage.messageSecurityInfo.messageSecurity == OTRMessageTransportSecurityOTR) {
844
        NSData *otrFingerprintData = baseMessage.messageSecurityInfo.otrFingerprint;
845
        if ([otrFingerprintData length]) {
846
            trusted = [[[OTRProtocolManager sharedInstance].encryptionManager otrFingerprintForKey:self.threadKey collection:self.threadCollection fingerprint:otrFingerprintData] isTrusted];
847
        }
848
    } else if (baseMessage.messageSecurityInfo.messageSecurity == OTRMessageTransportSecurityOMEMO) {
849
        NSString *omemoDeviceYapKey = baseMessage.messageSecurityInfo.omemoDeviceYapKey;
850
        NSString *omemoDeviceYapCollection = baseMessage.messageSecurityInfo.omemoDeviceYapCollection;
851
        __block OTROMEMODevice *device = nil;
852
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
853
            device = [transaction objectForKey:omemoDeviceYapKey inCollection:omemoDeviceYapCollection];
854
        }];
855
        if(device != nil) {
856
            trusted = [device isTrusted];
857
        }
858
    }
859
    return trusted;
860
}
861

    
862
- (BOOL) isGroupChat {
863
    __block OTRXMPPRoom *room = nil;
864
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
865
        room = [self roomWithTransaction:transaction];
866
    }];
867
    return (room != nil);
868
}
869

    
870
#pragma - mark Profile Button Methods
871

    
872
- (void)setupInfoButton {
873
    if ([self isGroupChat]) {
874
        UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"112-group" inBundle:[OTRAssets resourcesBundle] compatibleWithTraitCollection:nil] style:UIBarButtonItemStylePlain target:self action:@selector(didSelectOccupantsButton:)];
875
        self.navigationItem.rightBarButtonItem = barButtonItem;
876
    } else {
877
        UIButton* infoButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
878
        infoButton.accessibilityIdentifier = @"profileButton";
879
        [infoButton addTarget:self action:@selector(infoButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
880
        self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:infoButton];
881
    }
882
}
883

    
884
- (void) infoButtonPressed:(id)sender {
885
    __block OTRAccount *account = nil;
886
    __block OTRBuddy *buddy = nil;
887
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
888
        account = [self accountWithTransaction:transaction];
889
        buddy = [self buddyWithTransaction:transaction];
890
    }];
891
    if (!account || !buddy) {
892
        return;
893
    }
894
    
895
    // Hack to manually re-fetch OMEMO devicelist because PEP sucks
896
    // TODO: Ideally this should be moved to some sort of manual refresh in the Profile view
897
    [self fetchOMEMODeviceList];
898
    
899
    XLFormDescriptor *form = [UserProfileViewController profileFormDescriptorForAccount:account buddies:@[buddy] connection:self.readOnlyDatabaseConnection];
900

    
901
    UserProfileViewController *verify = [[UserProfileViewController alloc] initWithAccountKey:account.uniqueId connection:self.readOnlyDatabaseConnection form:form];
902
    verify.completionBlock = ^{
903
        [self updateEncryptionState];
904
    };
905
    UINavigationController *verifyNav = [[UINavigationController alloc] initWithRootViewController:verify];
906
    verifyNav.modalPresentationStyle = UIModalPresentationFormSheet;
907
    [self presentViewController:verifyNav animated:YES completion:nil];
908
}
909

    
910
- (void)didSelectOccupantsButton:(id)sender {
911
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"OTRRoomOccupants" bundle:[OTRAssets resourcesBundle]];
912
    OTRRoomOccupantsViewController *occupantsVC = [storyboard instantiateViewControllerWithIdentifier:@"roomOccupants"];
913
    occupantsVC.delegate = self;
914
    [occupantsVC setupViewHandlerWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection roomKey:self.threadKey];
915
    [self.navigationController pushViewController:occupantsVC animated:YES];
916
}
917

    
918
// Hack to manually re-fetch OMEMO devicelist because PEP sucks
919
// TODO: Ideally this should be moved to some sort of manual refresh in the Profile view
920
-(void) fetchOMEMODeviceList {
921
    __block OTRAccount *account = nil;
922
    __block OTRBuddy *buddy = nil;
923
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
924
        account = [self accountWithTransaction:transaction];
925
        buddy = [self buddyWithTransaction:transaction];
926
    }];
927
    if (!account || !buddy) {
928
        return;
929
    }
930
    id manager = [[OTRProtocolManager sharedInstance] protocolForAccount:account];
931
    if ([manager isKindOfClass:[OTRXMPPManager class]]) {
932
        XMPPJID *jid = [XMPPJID jidWithString:buddy.username];
933
        OTRXMPPManager *xmpp = manager;
934
        [xmpp.omemoSignalCoordinator.omemoModule fetchDeviceIdsForJID:jid elementId:nil];
935
    }
936
}
937

    
938
- (UIBarButtonItem *)rightBarButtonItem
939
{
940
    if (!self.lockBarButtonItem) {
941
        self.lockBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self.lockButton];
942
    }
943
    return self.lockBarButtonItem;
944
}
945

    
946
-(void)updateEncryptionState
947
{
948
    if ([self isGroupChat]) {
949
        __block OTRXMPPManager *xmpp = nil;
950
        [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
951
            xmpp = [self xmppManagerWithTransaction:transaction];
952
        } completionBlock:^{
953
            BOOL canSendMedia = NO;
954
            // Check for XEP-0363 HTTP upload
955
            // TODO: move this check elsewhere so it isnt dependent on refreshing crypto state
956
            if (xmpp != nil && xmpp.fileTransferManager.canUploadFiles) {
957
                canSendMedia = YES;
958
            }
959
            self.state.canSendMedia = canSendMedia;
960
            self.state.messageSecurity = OTRMessageTransportSecurityPlaintext;
961
            [self didUpdateState];
962
        }];
963
    } else {
964
        __block OTRBuddy *buddy = nil;
965
        __block OTRAccount *account = nil;
966
        __block OTRXMPPManager *xmpp = nil;
967
        __block OTRMessageTransportSecurity messageSecurity = OTRMessageTransportSecurityInvalid;
968
        
969
        [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
970
            buddy = [self buddyWithTransaction:transaction];
971
            account = [buddy accountWithTransaction:transaction];
972
            xmpp = [self xmppManagerWithTransaction:transaction];
973
            messageSecurity = [buddy preferredTransportSecurityWithTransaction:transaction];
974
        } completionBlock:^{
975
            BOOL canSendMedia = NO;
976
            // Check for XEP-0363 HTTP upload
977
            // TODO: move this check elsewhere so it isnt dependent on refreshing crypto state
978
            if (xmpp != nil && xmpp.fileTransferManager.canUploadFiles) {
979
                canSendMedia = YES;
980
            }
981
            if (!buddy || !account || !xmpp || (messageSecurity == OTRMessageTransportSecurityInvalid)) {
982
                DDLogError(@"updateEncryptionState error: missing parameters");
983
            } else {
984
                OTRKitMessageState messageState = [[OTRProtocolManager sharedInstance].encryptionManager.otrKit messageStateForUsername:buddy.username accountName:account.username protocol:account.protocolTypeString];
985
                if (messageState == OTRKitMessageStateEncrypted &&
986
                    buddy.status != OTRThreadStatusOffline) {
987
                    // If other side supports OTR, assume OTRDATA is possible
988
                    canSendMedia = YES;
989
                }
990
            }
991
            self.state.canSendMedia = canSendMedia;
992
            self.state.messageSecurity = messageSecurity;
993
            [self didUpdateState];
994
        }];
995
    }
996
}
997

    
998
- (void)setupAccessoryButtonsWithMessageState:(OTRKitMessageState)messageState buddyStatus:(OTRThreadStatus)status textViewHasText:(BOOL)hasText
999
{
1000
    self.inputToolbar.contentView.rightBarButtonItem = self.sendButton;
1001
    self.inputToolbar.sendButtonLocation = JSQMessagesInputSendButtonLocationRight;
1002
    self.inputToolbar.contentView.leftBarButtonItem = nil;
1003
}
1004

    
1005
- (void)connectButtonPressed:(id)sender
1006
{
1007
    [self hideDropdownAnimated:YES completion:nil];
1008
    __block OTRAccount *account = nil;
1009
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1010
        account = [self accountWithTransaction:transaction];
1011
    }];
1012
    
1013
    if (account == nil) {
1014
        return;
1015
    }
1016
    
1017
    //If we have the password then we can login with that password otherwise show login UI to enter password
1018
    if ([account.password length]) {
1019
        [[OTRProtocolManager sharedInstance] loginAccount:account userInitiated:YES];
1020
        
1021
    } else {
1022
        OTRBaseLoginViewController *loginViewController = [[OTRBaseLoginViewController alloc] initWithAccount:account];
1023
        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:loginViewController];
1024
        nav.modalPresentationStyle = UIModalPresentationFormSheet;
1025
        [self presentViewController:nav animated:YES completion:nil];
1026
    }
1027
    
1028
    
1029
}
1030

    
1031
#pragma - mark  dropDown Methods
1032

    
1033
- (void)showDropdownWithTitle:(NSString *)title buttons:(NSArray *)buttons animated:(BOOL)animated tag:(NSInteger)tag
1034
{
1035
    NSTimeInterval duration = 0.3;
1036
    if (!animated) {
1037
        duration = 0.0;
1038
    }
1039
    
1040
    self.buttonDropdownView = [[OTRButtonView alloc] initWithTitle:title buttons:buttons];
1041
    self.buttonDropdownView.tag = tag;
1042
    
1043
    CGFloat height = [OTRButtonView heightForTitle:title width:self.view.bounds.size.width buttons:buttons];
1044
    
1045
    [self.view addSubview:self.buttonDropdownView];
1046
    
1047
    [self.buttonDropdownView autoSetDimension:ALDimensionHeight toSize:height];
1048
    [self.buttonDropdownView autoPinEdgeToSuperviewEdge:ALEdgeLeading];
1049
    [self.buttonDropdownView autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
1050
    self.buttonDropdownView.topLayoutConstraint = [self.buttonDropdownView autoPinToTopLayoutGuideOfViewController:self withInset:height*-1];
1051
    
1052
    [self.buttonDropdownView layoutIfNeeded];
1053
    
1054
    [UIView animateWithDuration:duration animations:^{
1055
        self.buttonDropdownView.topLayoutConstraint.constant = 0.0;
1056
        [self.buttonDropdownView layoutIfNeeded];
1057
    } completion:nil];
1058
    
1059
}
1060

    
1061
- (void)hideDropdownAnimated:(BOOL)animated completion:(void (^)(void))completion
1062
{
1063
    if (!self.buttonDropdownView) {
1064
        if (completion) {
1065
            completion();
1066
        }
1067
    }
1068
    else {
1069
        NSTimeInterval duration = 0.3;
1070
        if (!animated) {
1071
            duration = 0.0;
1072
        }
1073
        
1074
        [UIView animateWithDuration:duration animations:^{
1075
            CGFloat height = self.buttonDropdownView.frame.size.height;
1076
            self.buttonDropdownView.topLayoutConstraint.constant = height*-1;
1077
            [self.buttonDropdownView layoutIfNeeded];
1078
            
1079
        } completion:^(BOOL finished) {
1080
            if (finished) {
1081
                [self.buttonDropdownView removeFromSuperview];
1082
                self.buttonDropdownView = nil;
1083
            }
1084
            
1085
            if (completion) {
1086
                completion();
1087
            }
1088
        }];
1089
    }
1090
}
1091

    
1092
- (void)saveCurrentMessageText:(NSString *)text threadKey:(NSString *)key colleciton:(NSString *)collection
1093
{
1094
    if (![key length] || ![collection length]) {
1095
        return;
1096
    }
1097
    
1098
    [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
1099
        id <OTRThreadOwner> thread = [[transaction objectForKey:key inCollection:collection] copy];
1100
        if (thread == nil) {
1101
            // this can happen when we've just approved a contact, then the thread key
1102
            // might have changed.
1103
            return;
1104
        }
1105
        [thread setCurrentMessageText:text];
1106
        [transaction setObject:thread forKey:key inCollection:collection];
1107
        
1108
        //Send inactive chat State
1109
        OTRAccount *account = [OTRAccount fetchObjectWithUniqueID:[thread threadAccountIdentifier] transaction:transaction];
1110
        OTRXMPPManager *xmppManager = (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account];
1111
        if (![text length]) {
1112
            [xmppManager sendChatState:OTRChatStateInactive withBuddyID:[thread threadIdentifier]];
1113
        }
1114
    }];
1115
}
1116

    
1117
//* Takes the current value out of the thread object and sets it to the text view and nils out result*/
1118
- (void)moveLastComposingTextForThreadKey:(NSString *)key colleciton:(NSString *)collection toTextView:(UITextView *)textView {
1119
    if (![key length] || ![collection length] || !textView) {
1120
        return;
1121
    }
1122
    __block id <OTRThreadOwner> thread = nil;
1123
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1124
        thread = [[transaction objectForKey:key inCollection:collection] copy];
1125
    }];
1126
    // Don't remove text you're already composing
1127
    NSString *oldThreadText = [thread currentMessageText];
1128
    if (!textView.text.length && oldThreadText.length) {
1129
        textView.text = oldThreadText;
1130
        [self receivedTextViewChanged:textView];
1131
    }
1132
    if (oldThreadText.length) {
1133
        [thread setCurrentMessageText:nil];
1134
        [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
1135
            [transaction setObject:thread forKey:key inCollection:collection];
1136
        }];
1137
    }
1138
}
1139

    
1140
- (id <OTRMessageProtocol,JSQMessageData>)messageAtIndexPath:(NSIndexPath *)indexPath
1141
{
1142
    // Multiple invocations with the same indexPath tend to come in groups, no need to hit the DB each time.
1143
    // Even though the object is cached, the row ID calculation still takes time
1144
    if (![indexPath isEqual:self.currentIndexPath]) {
1145
        self.currentIndexPath = indexPath;
1146
        self.currentMessage = [self.viewHandler object:indexPath];
1147
    }
1148
    return self.currentMessage;
1149
}
1150

    
1151
/**
1152
 * Updates the flexible range of the DB connection.
1153
 * @param reset When NO, adds kOTRMessagePageSize to the range length, when YES resets the length to the kOTRMessagePageSize
1154
 */
1155
- (void)updateRangeOptions:(BOOL)reset
1156
{
1157
    YapDatabaseViewRangeOptions *options = [self.viewHandler.mappings rangeOptionsForGroup:self.threadKey];
1158
    if (reset) {
1159
        if (options != nil && !self.messageRangeExtended) {
1160
            return;
1161
        }
1162
        options = [YapDatabaseViewRangeOptions flexibleRangeWithLength:kOTRMessagePageSize
1163
                                                                offset:0
1164
                                                                  from:YapDatabaseViewEnd];
1165
        self.messageSizeCache.countLimit = kOTRMessagePageSize;
1166
        self.messageRangeExtended = NO;
1167
    } else {
1168
        options = [options copyWithNewLength:options.length + kOTRMessagePageSize];
1169
        self.messageSizeCache.countLimit += kOTRMessagePageSize;
1170
        self.messageRangeExtended = YES;
1171
    }
1172
    [self.viewHandler.mappings setRangeOptions:options forGroup:self.threadKey];
1173
    
1174
    self.loadingMessages = YES;
1175
    
1176
    CGFloat distanceToBottom = self.collectionView.contentSize.height - self.collectionView.contentOffset.y;
1177
    
1178
    [self.collectionView reloadData];
1179
    
1180
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1181
        NSUInteger shownCount = [self.viewHandler.mappings numberOfItemsInGroup:self.threadKey];
1182
        NSUInteger totalCount = [[transaction ext:OTRFilteredChatDatabaseViewExtensionName] numberOfItemsInGroup:self.threadKey];
1183
        [self setShowLoadEarlierMessagesHeader:shownCount < totalCount];
1184
    }];
1185
    
1186
    if (!reset) {
1187
        [self.collectionView.collectionViewLayout invalidateLayout];
1188
        [self.collectionView layoutSubviews];
1189
        self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - distanceToBottom);
1190
    }
1191
    
1192
    self.loadingMessages = NO;
1193
}
1194

    
1195
- (BOOL)showDateAtIndexPath:(NSIndexPath *)indexPath
1196
{
1197
    BOOL showDate = NO;
1198
    if (indexPath.row == 0) {
1199
        showDate = YES;
1200
    }
1201
    else {
1202
        id <OTRMessageProtocol> currentMessage = [self messageAtIndexPath:indexPath];
1203
        id <OTRMessageProtocol> previousMessage = [self messageAtIndexPath:[NSIndexPath indexPathForItem:indexPath.row-1 inSection:indexPath.section]];
1204
        
1205
        NSTimeInterval timeDifference = [[currentMessage messageDate] timeIntervalSinceDate:[previousMessage messageDate]];
1206
        if (timeDifference > kOTRMessageSentDateShowTimeInterval) {
1207
            showDate = YES;
1208
        }
1209
    }
1210
    return showDate;
1211
}
1212

    
1213
- (BOOL)showSenderDisplayNameAtIndexPath:(NSIndexPath *)indexPath {
1214
    id<OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1215
    
1216
    if(![self.threadCollection isEqualToString:[OTRXMPPRoom collection]]) {
1217
        return NO;
1218
    }
1219
    
1220
    if ([[message senderId] isEqualToString:self.senderId]) {
1221
        return NO;
1222
    }
1223
    
1224
    if(indexPath.row -1 >= 0) {
1225
        NSIndexPath *previousIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:indexPath.section];
1226
        id<OTRMessageProtocol,JSQMessageData> previousMessage = [self messageAtIndexPath:previousIndexPath];
1227
        if ([[previousMessage senderId] isEqualToString:message.senderId]) {
1228
            return NO;
1229
        }
1230
    }
1231
    
1232
    return YES;
1233
}
1234

    
1235
- (BOOL)isPushMessageAtIndexPath:(NSIndexPath *)indexPath {
1236
    id message = [self messageAtIndexPath:indexPath];
1237
    return [message isKindOfClass:[PushMessage class]];
1238
}
1239

    
1240
- (void)receivedTextViewChangedNotification:(NSNotification *)notification
1241
{
1242
    //Check if the text state changes from having some text to some or vice versa
1243
    UITextView *textView = notification.object;
1244
    [self receivedTextViewChanged:textView];
1245
}
1246

    
1247
- (void)receivedTextViewChanged:(UITextView *)textView {
1248
    BOOL hasText = [textView.text length] > 0;
1249
    if(hasText != self.state.hasText) {
1250
        self.state.hasText = hasText;
1251
        [self didUpdateState];
1252
    }
1253
    
1254
    //Everytime the textview has text and a notification comes through we are 'typing' otherwise we are done typing
1255
    if (hasText) {
1256
        [self isTyping];
1257
    } else {
1258
        [self didFinishTyping];
1259
    }
1260
    
1261
    return;
1262

    
1263
}
1264

    
1265
#pragma - mark Update UI
1266

    
1267
- (void)didUpdateState {
1268
    
1269
}
1270

    
1271
- (void)isTyping {
1272
    
1273
}
1274

    
1275
- (void)didFinishTyping {
1276
    
1277
}
1278

    
1279
#pragma - mark Sending Media Items
1280

    
1281
- (void)sendMediaItem:(OTRMediaItem *)mediaItem data:(NSData *)data message:(id<OTRMessageProtocol>)message transaction:(YapDatabaseReadWriteTransaction *)transaction
1282
{
1283
    id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction];
1284
    OTRXMPPManager *xmpp = [self xmppManagerWithTransaction:transaction];
1285
    if (!message || !thread || !xmpp) {
1286
        DDLogError(@"Error sending file due to bad paramters");
1287
        return;
1288
    }
1289
    if (data) {
1290
        thread.lastMessageIdentifier = message.messageKey;
1291
        [thread saveWithTransaction:transaction];
1292
    }
1293
    // XEP-0363
1294
    [xmpp.fileTransferManager sendWithMediaItem:mediaItem prefetchedData:data message:message];
1295
    
1296
    [mediaItem touchParentMessageWithTransaction:transaction];
1297
}
1298

    
1299
#pragma - mark Media Display Methods
1300

    
1301
- (void)showImage:(OTRImageItem *)imageItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath
1302
{
1303
    //FIXME: Possible for image to not be in cache?
1304
    UIImage *image = [OTRImages imageWithIdentifier:imageItem.uniqueId];
1305
    JTSImageInfo *imageInfo = [[JTSImageInfo alloc] init];
1306
    imageInfo.image = image;
1307
    
1308
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
1309
    if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
1310
        UIView *cellContainterView = ((JSQMessagesCollectionViewCell *)cell).messageBubbleContainerView;
1311
        imageInfo.referenceRect = cellContainterView.bounds;
1312
        imageInfo.referenceView = cellContainterView;
1313
        imageInfo.referenceCornerRadius = 10;
1314
    }
1315
    
1316
    JTSImageViewController *imageViewer = [[JTSImageViewController alloc]
1317
                                           initWithImageInfo:imageInfo
1318
                                           mode:JTSImageViewControllerMode_Image
1319
                                           backgroundStyle:JTSImageViewControllerBackgroundOption_Blurred];
1320
    
1321
    [imageViewer showFromViewController:self transition:JTSImageViewControllerTransition_FromOriginalPosition];
1322
}
1323

    
1324
- (void)showVideo:(OTRVideoItem *)videoItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath
1325
{
1326
    if (videoItem.filename) {
1327
        NSURL *videoURL = [[OTRMediaServer sharedInstance] urlForMediaItem:videoItem buddyUniqueId:self.threadKey];
1328
        MPMoviePlayerViewController *moviePlayerViewController = [[MPMoviePlayerViewController alloc] initWithContentURL:videoURL];
1329
        [self presentViewController:moviePlayerViewController animated:YES completion:nil];
1330
    }
1331
}
1332

    
1333
- (void)playOrPauseAudio:(OTRAudioItem *)audioItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath
1334
{
1335
    NSError *error = nil;
1336
    if  ([audioItem.uniqueId isEqualToString:self.audioPlaybackController.currentAudioItem.uniqueId]) {
1337
        if  ([self.audioPlaybackController isPlaying]) {
1338
            [self.audioPlaybackController pauseCurrentlyPlaying];
1339
        }
1340
        else {
1341
            [self.audioPlaybackController resumeCurrentlyPlaying];
1342
        }
1343
    }
1344
    else {
1345
        [self.audioPlaybackController stopCurrentlyPlaying];
1346
        OTRAudioControlsView *audioControls = [self audioControllsfromCollectionView:collectionView atIndexPath:indexPath];
1347
        [self.audioPlaybackController attachAudioControlsView:audioControls];
1348
        [self.audioPlaybackController playAudioItem:audioItem buddyUniqueId:self.threadKey error:&error];
1349
    }
1350
    
1351
    if (error) {
1352
         DDLogError(@"Audio Playback Error: %@",error);
1353
    }
1354
   
1355
}
1356

    
1357
- (OTRAudioControlsView *)audioControllsfromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath {
1358
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
1359
    if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
1360
        UIView *mediaView = ((JSQMessagesCollectionViewCell *)cell).mediaView;
1361
        UIView *view = [mediaView viewWithTag:kOTRAudioControlsViewTag];
1362
        if ([view isKindOfClass:[OTRAudioControlsView class]]) {
1363
            return (OTRAudioControlsView *)view;
1364
        }
1365
    }
1366
    
1367
    return nil;
1368
}
1369

    
1370
#pragma MARK - OTRMessagesCollectionViewFlowLayoutSizeProtocol methods
1371

    
1372
- (BOOL)hasBubbleSizeForCellAtIndexPath:(NSIndexPath *)indexPath {
1373
    return ![self isPushMessageAtIndexPath:indexPath];
1374
}
1375

    
1376
#pragma mark - JSQMessagesViewController method overrides
1377

    
1378
- (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
1379
{
1380
    JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:collectionView cellForItemAtIndexPath:indexPath];
1381
    
1382
    //Fixes times when there needs to be two lines (date & knock sent) and doesn't seem to affect one line instances
1383
    cell.cellTopLabel.numberOfLines = 0;
1384
    
1385
    id <OTRMessageProtocol>message = [self messageAtIndexPath:indexPath];
1386
    
1387
    __block OTRXMPPAccount *account = nil;
1388
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1389
        account = (OTRXMPPAccount*)[self accountWithTransaction:transaction];
1390
    }];
1391
    
1392
    UIColor *textColor = nil;
1393
    if ([message isMessageIncoming]) {
1394
        textColor = [UIColor blackColor];
1395
    }
1396
    else {
1397
        textColor = [UIColor whiteColor];
1398
    }
1399
    if (cell.textView != nil)
1400
        cell.textView.textColor = textColor;
1401

    
1402
	// Do not allow clickable links for Tor accounts to prevent information leakage
1403
    // Could be better to move this information to the message object to not need to do a database read.
1404
    if ([account isKindOfClass:[OTRXMPPTorAccount class]]) {
1405
        cell.textView.dataDetectorTypes = UIDataDetectorTypeNone;
1406
    }
1407
    else {
1408
        cell.textView.dataDetectorTypes = UIDataDetectorTypeLink;
1409
        cell.textView.linkTextAttributes = @{ NSForegroundColorAttributeName : textColor,
1410
                                              NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) };
1411
    }
1412
    
1413
    if ([[message messageMediaItemKey] isEqualToString:self.audioPlaybackController.currentAudioItem.uniqueId]) {
1414
        UIView *view = [cell.mediaView viewWithTag:kOTRAudioControlsViewTag];
1415
        if ([view isKindOfClass:[OTRAudioControlsView class]]) {
1416
            [self.audioPlaybackController attachAudioControlsView:(OTRAudioControlsView *)view];
1417
        }
1418
    }
1419
    
1420
    // Needed for link interaction
1421
    cell.textView.delegate = self;
1422
    return cell;
1423
}
1424

    
1425
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
1426
{
1427
    if (action == @selector(delete:)) {
1428
        return YES;
1429
    }
1430
    
1431
    return [super collectionView:collectionView canPerformAction:action forItemAtIndexPath:indexPath withSender:sender];
1432
}
1433

    
1434
- (void)didPressSendButton:(UIButton *)button withMessageText:(NSString *)text senderId:(NSString *)senderId senderDisplayName:(NSString *)senderDisplayName date:(NSDate *)date
1435
{
1436
    if(!text.length) {
1437
        return;
1438
    }
1439
    
1440
    self.navigationController.providesPresentationContextTransitionStyle = YES;
1441
    self.navigationController.definesPresentationContext = YES;
1442
    
1443
    //0. Clear out message text immediately
1444
    //   This is to prevent the scenario where multiple messages get sent because the message text isn't cleared out
1445
    //   due to aggregated touch events during UI pauses.
1446
    //   A side effect is that sent messages may not appear in the UI immediately
1447
    [self finishSendingMessage];
1448
    
1449
    __block id<OTRMessageProtocol> message = nil;
1450
    __block OTRXMPPManager *xmpp = nil;
1451
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1452
        id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction];
1453
        message = [thread outgoingMessageWithText:text transaction:transaction];
1454
        xmpp = [self xmppManagerWithTransaction:transaction];
1455
    }];
1456
    if (!message || !xmpp) { return; }
1457
    [xmpp enqueueMessage:message];
1458
}
1459

    
1460
- (void)didPressAccessoryButton:(UIButton *)sender
1461
{
1462
    if ([sender isEqual:self.cameraButton]) {
1463
        [self.attachmentPicker showAlertControllerFromSourceView:sender withCompletion:nil];
1464
    }
1465
}
1466

    
1467
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
1468
{
1469
    if (action == @selector(delete:)) {
1470
        [self deleteMessageAtIndexPath:indexPath];
1471
    }
1472
    else {
1473
        [super collectionView:collectionView performAction:action forItemAtIndexPath:indexPath withSender:sender];
1474
    }
1475
}
1476

    
1477
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
1478
{
1479
    id <OTRMessageProtocol, JSQMessageData> message = [self messageAtIndexPath:indexPath];
1480

    
1481
    NSNumber *key = @(message.messageHash);
1482
    NSValue *sizeValue = [self.messageSizeCache objectForKey:key];
1483
    if (sizeValue != nil) {
1484
        return [sizeValue CGSizeValue];
1485
    }
1486

    
1487
    // Although JSQMessagesBubblesSizeCalculator has its own cache, its size is fixed and quite small, so it quickly chokes on scrolling into the past
1488
    CGSize size = [super collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:indexPath];
1489
    // The height of the first cell might change: on loading additional messages the date label most likely will disappear
1490
    if (indexPath.row > 0) {
1491
        [self.messageSizeCache setObject:[NSValue valueWithCGSize:size] forKey:key];
1492
    }
1493
    return size;
1494
}
1495

    
1496
#pragma - mark UIPopoverPresentationControllerDelegate Methods
1497

    
1498
- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController {
1499
    // Without setting this, there will be a crash on iPad
1500
    // This delegate is set in the OTRAttachmentPicker
1501
    popoverPresentationController.sourceView = self.cameraButton;
1502
}
1503

    
1504
- (void)sendPhoto:(UIImage *)photo asJPEG:(BOOL)asJPEG shouldResize:(BOOL)shouldResize {
1505
    NSParameterAssert(photo);
1506
    if (!photo) { return; }
1507
    __block OTRXMPPManager *xmpp = nil;
1508
    __block id<OTRThreadOwner> thread = nil;
1509
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1510
        xmpp = [self xmppManagerWithTransaction:transaction];
1511
        thread = [self threadObjectWithTransaction:transaction];
1512
    }];
1513
    NSParameterAssert(xmpp);
1514
    NSParameterAssert(thread);
1515
    if (!xmpp || !thread) { return; }
1516

    
1517
    [xmpp.fileTransferManager sendWithImage:photo thread:thread];
1518
}
1519

    
1520
#pragma - mark OTRAttachmentPickerDelegate Methods
1521

    
1522
- (void)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker gotPhoto:(UIImage *)photo withInfo:(NSDictionary *)info
1523
{
1524
    [self sendPhoto:photo asJPEG:YES shouldResize:YES];
1525
}
1526

    
1527
- (void)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker gotVideoURL:(NSURL *)videoURL
1528
{
1529
    if (!videoURL) { return; }
1530
    __block OTRXMPPManager *xmpp = nil;
1531
    __block id<OTRThreadOwner> thread = nil;
1532
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1533
        xmpp = [self xmppManagerWithTransaction:transaction];
1534
        thread = [self threadObjectWithTransaction:transaction];
1535
    }];
1536
    NSParameterAssert(xmpp);
1537
    NSParameterAssert(thread);
1538
    if (!xmpp || !thread) { return; }
1539

    
1540
    [xmpp.fileTransferManager sendWithVideoURL:videoURL thread:thread];
1541
}
1542

    
1543
- (NSArray <NSString *>*)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker preferredMediaTypesForSource:(UIImagePickerControllerSourceType)source
1544
{
1545
    return @[(NSString*)kUTTypeImage];
1546
}
1547

    
1548
- (void)sendAudioFileURL:(NSURL *)url
1549
{
1550
    if (!url) { return; }
1551
    __block OTRXMPPManager *xmpp = nil;
1552
    __block id<OTRThreadOwner> thread = nil;
1553
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1554
        xmpp = [self xmppManagerWithTransaction:transaction];
1555
        thread = [self threadObjectWithTransaction:transaction];
1556
    }];
1557
    NSParameterAssert(xmpp);
1558
    NSParameterAssert(thread);
1559
    if (!xmpp || !thread) { return; }
1560
    
1561
    [xmpp.fileTransferManager sendWithAudioURL:url thread:thread];
1562
}
1563

    
1564
- (void)sendImageFilePath:(NSString *)filePath asJPEG:(BOOL)asJPEG shouldResize:(BOOL)shouldResize
1565
{
1566
    [self sendPhoto:[UIImage imageWithContentsOfFile:filePath] asJPEG:asJPEG shouldResize:shouldResize];
1567
}
1568

    
1569

    
1570
#pragma - mark UIScrollViewDelegate Methods
1571

    
1572
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
1573
{
1574
    [self hideDropdownAnimated:YES completion:nil];
1575
}
1576

    
1577
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
1578
{
1579
    if (!self.loadingMessages) {
1580
        UIEdgeInsets insets = scrollView.contentInset;
1581
        CGFloat highestOffset = -insets.top;
1582
        CGFloat lowestOffset = scrollView.contentSize.height - scrollView.frame.size.height + insets.bottom;
1583
        CGFloat pos = scrollView.contentOffset.y;
1584

    
1585
        if (self.showLoadEarlierMessagesHeader && (pos == highestOffset || (pos < 0 && (scrollView.isDecelerating || scrollView.isDragging)))) {
1586
            [self updateRangeOptions:NO];
1587
        } else if (pos == lowestOffset) {
1588
            [self updateRangeOptions:YES];
1589
        }
1590
    }
1591
}
1592

    
1593
#pragma mark - UICollectionView DataSource
1594
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
1595
{
1596
    NSInteger numberOfMessages = [self.viewHandler.mappings numberOfItemsInSection:section];
1597
    return numberOfMessages;
1598
}
1599

    
1600
#pragma - mark JSQMessagesCollectionViewDataSource Methods
1601

    
1602
- (NSString *)senderDisplayName
1603
{
1604
    __block OTRAccount *account = nil;
1605
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1606
        account = [self accountWithTransaction:transaction];
1607
    }];
1608
    
1609
    NSString *senderDisplayName = @"";
1610
    if (account) {
1611
        if ([account.displayName length]) {
1612
            senderDisplayName = account.displayName;
1613
        } else {
1614
            senderDisplayName = account.username;
1615
        }
1616
    }
1617
    
1618
    return senderDisplayName;
1619
}
1620

    
1621
- (id<JSQMessageData>)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1622
{
1623
    return (id <JSQMessageData>)[self messageAtIndexPath:indexPath];
1624
}
1625

    
1626
- (id<JSQMessageBubbleImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1627
{
1628
    id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1629
    JSQMessagesBubbleImage *image = nil;
1630
    if ([message isMessageIncoming]) {
1631
        image = self.incomingBubbleImage;
1632
    }
1633
    else {
1634
        image = self.outgoingBubbleImage;
1635
    }
1636
    return image;
1637
}
1638

    
1639
- (id <JSQMessageAvatarImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1640
{
1641
    id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1642
    if ([message isKindOfClass:[PushMessage class]]) {
1643
        return nil;
1644
    }
1645
    
1646
    if (!message.isMessageIncoming) {
1647
        return [self accountAvatarImage];
1648
    }
1649
    
1650
    NSError *messageError = [message messageError];
1651
    if ((messageError && !messageError.isAutomaticDownloadError) ||
1652
        ![self isMessageTrusted:message]) {
1653
        return [self warningAvatarImage];
1654
    }
1655
    
1656
    if ([message isKindOfClass:[OTRXMPPRoomMessage class]]) {
1657
        OTRXMPPRoomMessage *roomMessage = (OTRXMPPRoomMessage *)message;
1658
        __block OTRXMPPRoomOccupant *roomOccupant = nil;
1659
        __block OTRXMPPBuddy *roomOccupantBuddy = nil;
1660
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1661
            roomOccupant = [OTRXMPPRoomOccupant occupantWithJid:[XMPPJID jidWithString:roomMessage.senderJID] realJID:[XMPPJID jidWithString:roomMessage.senderJID] roomJID:[XMPPJID jidWithString:roomMessage.roomJID] accountId:[self accountWithTransaction:transaction].uniqueId createIfNeeded:NO transaction:transaction];
1662
            if (roomOccupant != nil) {
1663
                roomOccupantBuddy = [roomOccupant buddyWith:transaction];
1664
            }
1665
        }];
1666
        UIImage *avatarImage = nil;
1667
        if (roomOccupant) {
1668
            if (roomOccupantBuddy != nil) {
1669
                avatarImage = [roomOccupantBuddy avatarImage];
1670
            }
1671
            if (!avatarImage) {
1672
                avatarImage = [roomOccupant avatarImage];
1673
            }
1674
        } else if (roomMessage.senderJID) {
1675
            avatarImage = [OTRImages avatarImageWithUsername:[[XMPPJID jidWithString:roomMessage.senderJID] resource]];
1676
        } else {
1677
            return nil;
1678
        }
1679
        if (avatarImage) {
1680
            NSUInteger diameter = MIN(avatarImage.size.width, avatarImage.size.height);
1681
            return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1682
        }
1683
    }
1684
    
1685
    if ([message isMessageIncoming]) {
1686
        return [self buddyAvatarImage];
1687
    }
1688

    
1689
    return [self accountAvatarImage];
1690
}
1691

    
1692
- (JSQMessagesAvatarImage *)createAvatarImage:(UIImage *(^)(YapDatabaseReadTransaction *))getImage
1693
{
1694
    __block UIImage *avatarImage;
1695
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1696
        avatarImage = getImage(transaction);
1697
    }];
1698
    if (avatarImage != nil) {
1699
        NSUInteger diameter = (NSUInteger) MIN(avatarImage.size.width, avatarImage.size.height);
1700
        return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1701
    }
1702
    return nil;
1703
}
1704

    
1705
- (JSQMessagesAvatarImage *)warningAvatarImage
1706
{
1707
    if (_warningAvatarImage == nil) {
1708
        _warningAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1709
            return [OTRImages circleWarningWithColor:[OTRColors warnColor]];
1710
        }];
1711
    }
1712
    return _warningAvatarImage;
1713
}
1714

    
1715
- (JSQMessagesAvatarImage *)accountAvatarImage
1716
{
1717
    if (_accountAvatarImage == nil) {
1718
        _accountAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1719
            return [[self accountWithTransaction:transaction] avatarImage];
1720
        }];
1721
    }
1722
    return _accountAvatarImage;
1723
}
1724

    
1725
- (JSQMessagesAvatarImage *)buddyAvatarImage
1726
{
1727
    if (_buddyAvatarImage == nil) {
1728
        _buddyAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1729
            return [[self buddyWithTransaction:transaction] avatarImage];
1730
        }];
1731
    }
1732
    return _buddyAvatarImage;
1733
}
1734

    
1735
////// Optional //////
1736

    
1737
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
1738
{
1739
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] init];
1740
    
1741
    if ([self showDateAtIndexPath:indexPath]) {
1742
        id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1743
        NSDate *date = [message messageDate];
1744
        if (date != nil) {
1745
            [text appendAttributedString: [[JSQMessagesTimestampFormatter sharedFormatter] attributedTimestampForDate:date]];
1746
        }
1747
    }
1748
    
1749
    if ([self isPushMessageAtIndexPath:indexPath]) {
1750
        JSQMessagesTimestampFormatter *formatter = [JSQMessagesTimestampFormatter sharedFormatter];
1751
        NSString *knockString = KNOCK_SENT_STRING();
1752
        //Add new line if there is already a date string
1753
        if ([text length] > 0) {
1754
            knockString = [@"\n" stringByAppendingString:knockString];
1755
        }
1756
        [text appendAttributedString:[[NSAttributedString alloc] initWithString:knockString attributes:formatter.dateTextAttributes]];
1757
    }
1758
    
1759
    return text;
1760
}
1761

    
1762

    
1763
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
1764
{
1765
    if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
1766
        id<OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1767
        
1768
        __block NSString *displayName = nil;
1769
        if ([message isKindOfClass:[OTRXMPPRoomMessage class]]) {
1770
            OTRXMPPRoomMessage *roomMessage = (OTRXMPPRoomMessage *)message;
1771
            [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1772
                OTRXMPPRoomOccupant *occupant = [OTRXMPPRoomOccupant occupantWithJid:[XMPPJID jidWithString:roomMessage.senderJID] realJID:[XMPPJID jidWithString:roomMessage.senderJID] roomJID:[XMPPJID jidWithString:roomMessage.roomJID] accountId:[self accountWithTransaction:transaction].uniqueId createIfNeeded:NO transaction:transaction];
1773
                if (occupant) {
1774
                    OTRXMPPBuddy *buddy = [occupant buddyWith:transaction];
1775
                    if (buddy) {
1776
                        displayName = [buddy displayName];
1777
                    } else {
1778
                        displayName = [[XMPPJID jidWithString:occupant.jid] resource];
1779
                    }
1780
                }
1781
            }];
1782
        }
1783
        if (!displayName) {
1784
            displayName = [message senderDisplayName];
1785
        }
1786
        return [[NSAttributedString alloc] initWithString:displayName];
1787
    }
1788
    
1789
    return  nil;
1790
}
1791

    
1792
/** Currently uses clock for queued, and checkmark for delivered. */
1793
- (nullable NSAttributedString*) deliveryStatusStringForMessage:(nonnull id<OTRMessageProtocol>)message {
1794
    if (!message) { return nil; }
1795
    // Only applies to outgoing messages
1796
    if ([message isMessageIncoming]) {
1797
        return nil;
1798
    }
1799
    NSString *deliveryStatusString = nil;
1800
    if(message.isMessageSent == NO && ![message messageMediaItemKey]) {
1801
        // Waiting to send message. This message is in the queue.
1802
        deliveryStatusString = [NSString fa_stringForFontAwesomeIcon:FAClockO];
1803
    } else if (message.isMessageDelivered){
1804
        deliveryStatusString = [NSString stringWithFormat:@"%@ ",[NSString fa_stringForFontAwesomeIcon:FACheck]];
1805
    }
1806
    if (deliveryStatusString != nil) {
1807
        UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12];
1808
        if (!font) {
1809
            font = [UIFont systemFontOfSize:12];
1810
        }
1811
        return [[NSAttributedString alloc] initWithString:deliveryStatusString attributes:@{NSFontAttributeName: font}];
1812
    }
1813
    return nil;
1814
}
1815

    
1816
- (nullable NSAttributedString *) encryptionStatusStringForMessage:(nonnull id<OTRMessageProtocol>)message {
1817
    NSString *lockString = nil;
1818
    if (message.messageSecurity == OTRMessageTransportSecurityOTR) {
1819
        lockString = [NSString stringWithFormat:@"%@ OTR ",[NSString fa_stringForFontAwesomeIcon:FALock]];
1820
    } else if (message.messageSecurity == OTRMessageTransportSecurityOMEMO) {
1821
        lockString = [NSString stringWithFormat:@"%@ OMEMO ",[NSString fa_stringForFontAwesomeIcon:FALock]];
1822
    }
1823
    else {
1824
        lockString = [NSString fa_stringForFontAwesomeIcon:FAUnlock];
1825
    }
1826
    UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12];
1827
    if (!font) {
1828
        font = [UIFont systemFontOfSize:12];
1829
    }
1830
    return [[NSAttributedString alloc] initWithString:lockString attributes:@{NSFontAttributeName: font}];
1831
}
1832

    
1833

    
1834
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
1835
{
1836
    id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1837
    if (!message) {
1838
        return [[NSAttributedString alloc] initWithString:@""];
1839
    }
1840
    
1841
    UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12];
1842
    if (!font) {
1843
        font = [UIFont systemFontOfSize:12];
1844
    }
1845
    NSDictionary *iconAttributes = @{NSFontAttributeName: font};
1846
    NSDictionary *lockAttributes = [iconAttributes copy];
1847
    
1848
    ////// Lock Icon //////
1849
    NSAttributedString *lockString = [self encryptionStatusStringForMessage:message];
1850
    if (!lockString) {
1851
        lockString = [[NSAttributedString alloc] initWithString:@""];
1852
    }
1853
    NSMutableAttributedString *attributedString = [lockString mutableCopy];
1854
    
1855
    BOOL trusted = YES;
1856
    if([message isKindOfClass:[OTRBaseMessage class]]) {
1857
        trusted = [self isMessageTrusted:message];
1858
    };
1859
    
1860
    if (!trusted) {
1861
        NSMutableDictionary *mutableCopy = [lockAttributes mutableCopy];
1862
        [mutableCopy setObject:[UIColor redColor] forKey:NSForegroundColorAttributeName];
1863
        lockAttributes = mutableCopy;
1864
    }
1865
    
1866
    NSAttributedString *deliveryString = [self deliveryStatusStringForMessage:message];
1867
    if (deliveryString) {
1868
        [attributedString appendAttributedString:deliveryString];
1869
    }
1870
    
1871
    if([[message messageMediaItemKey] length] > 0) {
1872
        
1873
        __block OTRMediaItem *mediaItem = nil;
1874
        //Get the media item
1875
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
1876
            mediaItem = [OTRMediaItem fetchObjectWithUniqueID:[message messageMediaItemKey] transaction:transaction];
1877
        }];
1878
        if (!mediaItem) {
1879
            return attributedString;
1880
        }
1881
        
1882
        float percentProgress = mediaItem.transferProgress * 100;
1883
        
1884
        NSString *progressString = nil;
1885
        NSUInteger insertIndex = 0;
1886
        
1887
        if (mediaItem.isIncoming && mediaItem.transferProgress < 1) {
1888
            if (message.messageError) {
1889
                progressString = [NSString stringWithFormat:@"%@ ",WAITING_STRING()];
1890
            } else {
1891
                progressString = [NSString stringWithFormat:@" %@ %.0f%%",INCOMING_STRING(),percentProgress];
1892
            }
1893
            insertIndex = [attributedString length];
1894
        } else if (!mediaItem.isIncoming && mediaItem.transferProgress < 1) {
1895
            if(percentProgress > 0) {
1896
                progressString = [NSString stringWithFormat:@"%@ %.0f%% ",SENDING_STRING(),percentProgress];
1897
            } else {
1898
                progressString = [NSString stringWithFormat:@"%@ ",WAITING_STRING()];
1899
            }
1900
        }
1901
        
1902
        if ([progressString length]) {
1903
            UIFont *font = [UIFont systemFontOfSize:12];
1904
            [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:progressString attributes:@{NSFontAttributeName: font}] atIndex:insertIndex];
1905
        }
1906
    }
1907
    
1908
    return attributedString;
1909
}
1910

    
1911

    
1912
#pragma - mark  JSQMessagesCollectionViewDelegateFlowLayout Methods
1913

    
1914
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1915
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1916
heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
1917
{
1918
    CGFloat height = 0.0f;
1919
    if ([self showDateAtIndexPath:indexPath]) {
1920
        height += kJSQMessagesCollectionViewCellLabelHeightDefault;
1921
    }
1922
    
1923
    if ([self isPushMessageAtIndexPath:indexPath]) {
1924
        height += kJSQMessagesCollectionViewCellLabelHeightDefault;
1925
    }
1926
    return height;
1927
}
1928

    
1929
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1930
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1931
heightForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
1932
{
1933
    if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
1934
        return kJSQMessagesCollectionViewCellLabelHeightDefault;
1935
    }
1936
    return 0.0f;
1937
}
1938

    
1939
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1940
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1941
heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
1942
{
1943
    CGFloat height = kJSQMessagesCollectionViewCellLabelHeightDefault;
1944
    if ([self isPushMessageAtIndexPath:indexPath]) {
1945
        height = 0.0f;
1946
    }
1947
    return height;
1948
}
1949

    
1950
- (void)deleteMessageAtIndexPath:(NSIndexPath *)indexPath
1951
{
1952
    __block id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1953
    __weak __typeof__(self) weakSelf = self;
1954
    [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
1955
        __typeof__(self) strongSelf = weakSelf;
1956
        [transaction removeObjectForKey:[message messageKey] inCollection:[message messageCollection]];
1957
        //Update Last message date for sorting and grouping
1958
        OTRBuddy *buddy = [[strongSelf buddyWithTransaction:transaction] copy];
1959
        buddy.lastMessageId = nil;
1960
        [buddy saveWithTransaction:transaction];
1961
    }];
1962
}
1963

    
1964
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapAvatarImageView:(UIImageView *)avatarImageView atIndexPath:(NSIndexPath *)indexPath
1965
{
1966
    id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1967
    [self didTapAvatar:message sender:avatarImageView];
1968
}
1969

    
1970
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapMessageBubbleAtIndexPath:(NSIndexPath *)indexPath
1971
{
1972
    id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1973
    if (!message.isMediaMessage) {
1974
        return;
1975
    }
1976
    __block OTRMediaItem *item = nil;
1977
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
1978
         item = [OTRMediaItem mediaItemForMessage:message transaction:transaction];
1979
    }];
1980
    if (!item) { return; }
1981
    if (item.transferProgress != 1 && item.isIncoming) {
1982
        return;
1983
    }
1984
    
1985
    if ([item isKindOfClass:[OTRImageItem class]]) {
1986
        [self showImage:(OTRImageItem *)item fromCollectionView:collectionView atIndexPath:indexPath];
1987
    }
1988
    else if ([item isKindOfClass:[OTRVideoItem class]]) {
1989
        [self showVideo:(OTRVideoItem *)item fromCollectionView:collectionView atIndexPath:indexPath];
1990
    }
1991
    else if ([item isKindOfClass:[OTRAudioItem class]]) {
1992
        [self playOrPauseAudio:(OTRAudioItem *)item fromCollectionView:collectionView atIndexPath:indexPath];
1993
    } else if ([message conformsToProtocol:@protocol(OTRDownloadMessage)]) {
1994
        id<OTRDownloadMessage> download = (id<OTRDownloadMessage>)message;
1995
        // Janky hack to open URL for now
1996
        NSArray<UIAlertAction*> *actions = [UIAlertAction actionsForMediaMessage:download sourceView:self.view viewController:self];
1997
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:message.text message:nil preferredStyle:UIAlertControllerStyleActionSheet];
1998
        [actions enumerateObjectsUsingBlock:^(UIAlertAction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
1999
            [alert addAction:obj];
2000
        }];
2001
        [alert addAction:[self cancleAction]];
2002
        
2003
        // Get the anchor
2004
        alert.popoverPresentationController.sourceView = self.view;
2005
        alert.popoverPresentationController.sourceRect = self.view.bounds;
2006
        UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
2007
        if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
2008
            UIView *cellContainterView = ((JSQMessagesCollectionViewCell *)cell).messageBubbleContainerView;
2009
            alert.popoverPresentationController.sourceRect = cellContainterView.bounds;
2010
            alert.popoverPresentationController.sourceView = cellContainterView;
2011
        }
2012

    
2013
        [self presentViewController:alert animated:YES completion:nil];
2014
    }
2015
}
2016

    
2017
#pragma - mark database view delegate
2018

    
2019
- (void)didSetupMappings:(OTRYapViewHandler *)handler
2020
{
2021
    // The databse view is setup now so refresh from there
2022
    [self updateViewWithKey:self.threadKey collection:self.threadCollection];
2023
    [self updateRangeOptions:YES];
2024
    [self.collectionView reloadData];
2025
}
2026

    
2027
- (void)didReceiveChanges:(OTRYapViewHandler *)handler key:(NSString *)key collection:(NSString *)collection
2028
{
2029
    [self updateViewWithKey:key collection:collection];
2030
}
2031

    
2032
- (void)didReceiveChanges:(OTRYapViewHandler *)handler sectionChanges:(NSArray<YapDatabaseViewSectionChange *> *)sectionChanges rowChanges:(NSArray<YapDatabaseViewRowChange *> *)rowChanges
2033
{
2034
    if (!rowChanges.count) {
2035
        return;
2036
    }
2037
    
2038
    // Important to clear our "one message cache" here, since things may have changed.
2039
    self.currentIndexPath = nil;
2040
    
2041
    NSUInteger collectionViewNumberOfItems = [self.collectionView numberOfItemsInSection:0];
2042
    NSUInteger numberMappingsItems = [self.viewHandler.mappings numberOfItemsInSection:0];
2043
    
2044
    [self.collectionView performBatchUpdates:^{
2045
        
2046
        for (YapDatabaseViewRowChange *rowChange in rowChanges)
2047
        {
2048
            switch (rowChange.type)
2049
            {
2050
                case YapDatabaseViewChangeDelete :
2051
                {
2052
                    [self.collectionView deleteItemsAtIndexPaths:@[rowChange.indexPath]];
2053
                    break;
2054
                }
2055
                case YapDatabaseViewChangeInsert :
2056
                {
2057
                    [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]];
2058
                    break;
2059
                }
2060
                case YapDatabaseViewChangeMove :
2061
                {
2062
                    [self.collectionView moveItemAtIndexPath:rowChange.indexPath toIndexPath:rowChange.newIndexPath];
2063
                    break;
2064
                }
2065
                case YapDatabaseViewChangeUpdate :
2066
                {
2067
                    // Update could be e.g. when we are done auto-loading a link. We
2068
                    // need to reset the stored size of this item, so the image/message
2069
                    // will get the correct bubble height.
2070
                    id <JSQMessageData> message = [self messageAtIndexPath:rowChange.indexPath];
2071
                    [self.collectionView.collectionViewLayout.bubbleSizeCalculator resetBubbleSizeCacheForMessageData:message];
2072
                    [self.messageSizeCache removeObjectForKey:@(message.messageHash)];
2073
                    [self.collectionView reloadItemsAtIndexPaths:@[ rowChange.indexPath]];
2074
                    break;
2075
                }
2076
            }
2077
        }
2078
    } completion:^(BOOL finished){
2079
        if(numberMappingsItems > collectionViewNumberOfItems && numberMappingsItems > 0) {
2080
            //Inserted new item, probably at the end
2081
            //Get last message and test if isIncoming
2082
            NSIndexPath *lastMessageIndexPath = [NSIndexPath indexPathForRow:numberMappingsItems - 1 inSection:0];
2083
            id <OTRMessageProtocol>lastMessage = [self messageAtIndexPath:lastMessageIndexPath];
2084
            if ([lastMessage isMessageIncoming]) {
2085
                [self finishReceivingMessage];
2086
            } else {
2087
                // We can't use finishSendingMessage here because it might
2088
                // accidentally clear out unsent message text
2089
                [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
2090
                [self scrollToBottomAnimated:YES];
2091
            }
2092
        } else {
2093
            [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
2094
        }
2095
    }];
2096
}
2097

    
2098
#pragma - mark UITextViewDelegateMethods
2099

    
2100
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
2101
{
2102
    if ([URL otr_isInviteLink]) {
2103
        NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
2104
        activity.webpageURL = URL;
2105
        [[OTRAppDelegate appDelegate] application:[UIApplication sharedApplication] continueUserActivity:activity restorationHandler:^(NSArray * _Nullable restorableObjects) {
2106
            // TODO: restore stuff
2107
        }];
2108
        return NO;
2109
    }
2110
    
2111
    UIActivityViewController *activityViewController = [UIActivityViewController otr_linkActivityViewControllerWithURLs:@[URL]];
2112
    
2113
    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
2114
        activityViewController.popoverPresentationController.sourceView = textView;
2115
        activityViewController.popoverPresentationController.sourceRect = textView.bounds;
2116
    }
2117
    
2118
    [self presentViewController:activityViewController animated:YES completion:nil];
2119
    return NO;
2120
}
2121

    
2122
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
2123
    return NO;
2124
}
2125

    
2126
- (void)viewWillLayoutSubviews {
2127
    self.currentIndexPath = nil;
2128
    [super viewWillLayoutSubviews];
2129
}
2130

    
2131
- (void)viewDidLayoutSubviews {
2132
    [super viewDidLayoutSubviews];
2133
    [self layoutJIDForwardingHeader];
2134
}
2135

    
2136
#pragma - mark Buddy Migration methods
2137

    
2138
- (nullable XMPPJID *)getForwardingJIDForBuddy:(OTRXMPPBuddy *)xmppBuddy {
2139
    XMPPJID *ret = nil;
2140
    if (xmppBuddy != nil && xmppBuddy.vCardTemp != nil) {
2141
        ret = xmppBuddy.vCardTemp.jid;
2142
    }
2143
    return ret;
2144
}
2145

    
2146
- (void)layoutJIDForwardingHeader {
2147
    if (self.jidForwardingHeaderView != nil) {
2148
        [self.jidForwardingHeaderView setNeedsLayout];
2149
        [self.jidForwardingHeaderView layoutIfNeeded];
2150
        int height = [self.jidForwardingHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 1;
2151
        self.jidForwardingHeaderView.frame = CGRectMake(0, self.topLayoutGuide.length, self.view.frame.size.width, height);
2152
        [self.view bringSubviewToFront:self.jidForwardingHeaderView];
2153
        self.topContentAdditionalInset = height;
2154
    }
2155
}
2156

    
2157
- (void)updateJIDForwardingHeader {
2158
    
2159
    __block id<OTRThreadOwner> thread = nil;
2160
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2161
        thread = [self threadObjectWithTransaction:transaction];
2162
    }];
2163
    OTRXMPPBuddy *buddy = nil;
2164
    if ([thread isKindOfClass:[OTRXMPPBuddy class]]) {
2165
        buddy = (OTRXMPPBuddy*)thread;
2166
    }
2167
    
2168
    // If we have a buddy with vcard JID set to something else than the username, show a
2169
    // "buddy has moved" warning to allow the user to start a chat with that JID instead.
2170
    BOOL showHeader = NO;
2171
    XMPPJID *forwardingJid = [self getForwardingJIDForBuddy:buddy];
2172
    if (forwardingJid != nil && ![forwardingJid isEqualToJID:buddy.bareJID options:XMPPJIDCompareBare]) {
2173
        showHeader = YES;
2174
    }
2175
    
2176
    if (showHeader) {
2177
        [self showJIDForwardingHeaderWithNewJID:forwardingJid];
2178
    } else if (!showHeader && self.jidForwardingHeaderView != nil) {
2179
        self.topContentAdditionalInset = 0;
2180
        [self.jidForwardingHeaderView removeFromSuperview];
2181
        self.jidForwardingHeaderView = nil;
2182
    }
2183
}
2184

    
2185
- (void)showJIDForwardingHeaderWithNewJID:(XMPPJID *)newJid {
2186
    if (self.jidForwardingHeaderView == nil) {
2187
        UINib *nib = [UINib nibWithNibName:@"MigratedBuddyHeaderView" bundle:OTRAssets.resourcesBundle];
2188
        MigratedBuddyHeaderView *header = (MigratedBuddyHeaderView*)[nib instantiateWithOwner:self options:nil][0];
2189
        [header setForwardingJID:newJid];
2190
        [header.titleLabel setText:MIGRATED_BUDDY_STRING()];
2191
        [header.descriptionLabel setText:MIGRATED_BUDDY_INFO_STRING()];
2192
        [header.switchButton setTitle:MIGRATED_BUDDY_SWITCH() forState:UIControlStateNormal];
2193
        [header.ignoreButton setTitle:MIGRATED_BUDDY_IGNORE() forState:UIControlStateNormal];
2194
        [header setBackgroundColor:UIColor.whiteColor];
2195
        [self.view addSubview:header];
2196
        [self.view bringSubviewToFront:header];
2197
        self.jidForwardingHeaderView = header;
2198
        [self.view setNeedsLayout];
2199
    }
2200
}
2201

    
2202
- (IBAction)didPressMigratedIgnore {
2203
    if (self.jidForwardingHeaderView != nil) {
2204
        self.jidForwardingHeaderView.hidden = YES;
2205
        self.topContentAdditionalInset = 0;
2206
    }
2207
}
2208

    
2209
- (IBAction)didPressMigratedSwitch {
2210
    if (self.jidForwardingHeaderView != nil) {
2211
        self.jidForwardingHeaderView.hidden = YES;
2212
        self.topContentAdditionalInset = 0;
2213
    }
2214
    
2215
    __block OTRXMPPBuddy *buddy = nil;
2216
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2217
        buddy = (OTRXMPPBuddy*)[self buddyWithTransaction:transaction];
2218
    }];
2219
    
2220
    XMPPJID *forwardingJid = [self getForwardingJIDForBuddy:buddy];
2221
    if (forwardingJid != nil) {
2222
        // Try to find buddy
2223
        //
2224
        [[OTRDatabaseManager sharedInstance].readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
2225
            OTRAccount *account = [self accountWithTransaction:transaction];
2226
            OTRXMPPBuddy *buddy = [OTRXMPPBuddy fetchBuddyWithJid:forwardingJid accountUniqueId:account.uniqueId transaction:transaction];
2227
            if (!buddy) {
2228
                buddy = [[OTRXMPPBuddy alloc] init];
2229
                buddy.accountUniqueId = account.uniqueId;
2230
                buddy.username = forwardingJid.bare;
2231
                [buddy saveWithTransaction:transaction];
2232
                id<OTRProtocol> proto = [[OTRProtocolManager sharedInstance] protocolForAccount:account];
2233
                if (proto != nil) {
2234
                    [proto addBuddy:buddy];
2235
                }
2236
            }
2237
            [self setThreadKey:buddy.uniqueId collection:[OTRBuddy collection]];
2238
        }];
2239
    }
2240
}
2241

    
2242
#pragma - mark Group chat support
2243

    
2244
- (void)setupWithBuddies:(NSArray<NSString *> *)buddies accountId:(NSString *)accountId name:(NSString *)name
2245
{
2246
    __block OTRXMPPAccount *account = nil;
2247
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2248
        account = [OTRXMPPAccount fetchObjectWithUniqueID:accountId transaction:transaction];
2249
    }];
2250
    OTRXMPPManager *xmppManager = (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account];
2251
    NSString *service = [xmppManager.roomManager.conferenceServicesJID firstObject];
2252
    if (service.length > 0) {
2253
        NSString *roomName = [NSUUID UUID].UUIDString;
2254
        XMPPJID *roomJID = [XMPPJID jidWithString:[NSString stringWithFormat:@"%@@%@",roomName,service]];
2255
        self.threadKey = [xmppManager.roomManager startGroupChatWithBuddies:buddies roomJID:roomJID nickname:account.displayName subject:name];
2256
        [self setThreadKey:self.threadKey collection:[OTRXMPPRoom collection]];
2257
    } else {
2258
        DDLogError(@"No conference server for account: %@", account.username);
2259
    }
2260
}
2261

    
2262
#pragma - mark OTRRoomOccupantsViewControllerDelegate
2263

    
2264
- (void)didLeaveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
2265
    __block OTRXMPPRoom *room = nil;
2266
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2267
        room = [self roomWithTransaction:transaction];
2268
    }];
2269
    if (room) {
2270
        [self setThreadKey:nil collection:nil];
2271
        [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
2272
            [room removeWithTransaction:transaction];
2273
        }];
2274
    }
2275
    [self.navigationController popViewControllerAnimated:NO];
2276
    if ([[self.navigationController viewControllers] count] > 1) {
2277
        [self.navigationController popViewControllerAnimated:YES];
2278
    } else {
2279
        [self.navigationController.navigationController popViewControllerAnimated:YES];
2280
    }
2281
    
2282
}
2283

    
2284
- (void)didArchiveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
2285
    __block OTRXMPPRoom *room = nil;
2286
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2287
        room = [self roomWithTransaction:transaction];
2288
    }];
2289
    if (room) {
2290
        [self setThreadKey:nil collection:nil];
2291
        [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
2292
            room.isArchived = YES;
2293
            [room saveWithTransaction:transaction];
2294
        }];
2295
    }
2296
    [self.navigationController popViewControllerAnimated:NO];
2297
    if ([[self.navigationController viewControllers] count] > 1) {
2298
        [self.navigationController popViewControllerAnimated:YES];
2299
    } else {
2300
        [self.navigationController.navigationController popViewControllerAnimated:YES];
2301
    }
2302
}
2303
@end