Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / View Controllers / OTRMessagesViewController.m @ 8358d691

History | View | Annotate | Download (96.9 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
}
227

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

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

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

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

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

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

    
323
#pragma - mark Setters & getters
324

    
325
- (OTRAttachmentPicker *)attachmentPicker
326
{
327
    if (!_attachmentPicker) {
328
        _attachmentPicker = [[OTRAttachmentPicker alloc] initWithParentViewController:self delegate:self];
329
    }
330
    return _attachmentPicker;
331
}
332

    
333
- (NSArray*) indexPathsToCount:(NSUInteger)count {
334
    NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:count];
335
    for (NSUInteger i = 0; i < count; i++) {
336
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
337
        [indexPaths addObject:indexPath];
338
    }
339
    return indexPaths;
340
}
341

    
342
- (nullable id<OTRThreadOwner>)threadObjectWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
343
    if (!self.threadKey || !self.threadCollection || !transaction) { return nil; }
344
    id object = [transaction objectForKey:self.threadKey inCollection:self.threadCollection];
345
    if ([object conformsToProtocol:@protocol(OTRThreadOwner)]) {
346
        return object;
347
    }
348
    return nil;
349
}
350

    
351
- (nullable OTRBuddy *)buddyWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
352
    id <OTRThreadOwner> object = [self threadObjectWithTransaction:transaction];
353
    if ([object isKindOfClass:[OTRBuddy class]]) {
354
        return (OTRBuddy *)object;
355
    }
356
    return nil;
357
}
358

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

    
367
- (nullable OTRAccount *)accountWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
368
    id <OTRThreadOwner> thread =  [self threadObjectWithTransaction:transaction];
369
    if (!thread) { return nil; }
370
    OTRAccount *account = [OTRAccount fetchObjectWithUniqueID:[thread threadAccountIdentifier] transaction:transaction];
371
    return account;
372
}
373

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

    
459
                           
460
- (YapDatabaseConnection *)readOnlyDatabaseConnection
461
{
462
    if (!_readOnlyDatabaseConnection) {
463
        _readOnlyDatabaseConnection = [OTRDatabaseManager sharedInstance].readOnlyDatabaseConnection;
464
    }
465
    return _readOnlyDatabaseConnection;
466
}
467
                           
468
- (YapDatabaseConnection *)readWriteDatabaseConnection
469
{
470
    if (!_readWriteDatabaseConnection) {
471
            _readWriteDatabaseConnection = [OTRDatabaseManager sharedInstance].readWriteDatabaseConnection;
472
    }
473
    return _readWriteDatabaseConnection;
474
}
475
                        
476

    
477
- (nullable OTRXMPPManager *)xmppManagerWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
478
    OTRAccount *account = [self accountWithTransaction:transaction];
479
    if (!account) { return nil; }
480
    return (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account];
481
}
482

    
483
/** Will send a probe to fetch last seen */
484
- (void) sendPresenceProbe {
485
    __block OTRXMPPManager *xmpp = nil;
486
    __block OTRXMPPBuddy *buddy = nil;
487
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
488
        xmpp = [self xmppManagerWithTransaction:transaction];
489
        buddy = (OTRXMPPBuddy*)[self buddyWithTransaction:transaction];
490
    }];
491
    if (!xmpp || ![buddy isKindOfClass:[OTRXMPPBuddy class]] || buddy.pendingApproval) { return; }
492
    [xmpp sendPresenceProbeForBuddy:buddy];
493
}
494

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

    
536
        // Auto-inititate OTR when contact comes online
537
        if (!previousState && self.state.isThreadOnline) {
538
            [[OTRProtocolManager sharedInstance].encryptionManager maybeRefreshOTRSessionForBuddyKey:key collection:collection];
539
        }
540
    } else if ([collection isEqualToString:[OTRXMPPRoom collection]]) {
541
        __block OTRXMPPRoom *room = nil;
542
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
543
            room = [OTRXMPPRoom fetchObjectWithUniqueID:key transaction:transaction];
544
        }];
545
        self.state.isThreadOnline = room.currentStatus != OTRThreadStatusOffline;
546
        [self didUpdateState];
547
        [self refreshTitleView:[self titleView]];
548
    }
549
    [self tryToMarkAllMessagesAsRead];
550
}
551

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

    
594
- (OTRTitleSubtitleView * __nonnull)titleView {
595
    UIView *titleView = self.navigationItem.titleView;
596
    if ([titleView isKindOfClass:[OTRTitleSubtitleView class]]) {
597
        return  (OTRTitleSubtitleView*)titleView;
598
    }
599
    return [[OTRTitleSubtitleView alloc] initWithFrame:CGRectMake(0, 0, 200, 44)];
600
}
601

    
602
- (void)refreshTitleTimerUpdate:(NSTimer*)timer {
603
    [self refreshTitleView:[self titleView]];
604
}
605

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

    
674
}
675

    
676
/**
677
 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.
678
 */
679
- (UIAlertAction *)resendOutgoingMessageActionForMessageKey:(NSString *)messageKey
680
                                          messageCollection:(NSString *)messageCollection
681
                                readWriteDatabaseConnection:(YapDatabaseConnection*)databaseConnection
682
                                                      title:(NSString *)title
683
{
684
    UIAlertAction *action = [UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
685
        [databaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
686
            id object = [[transaction objectForKey:messageKey inCollection:messageCollection] copy];
687
            id<OTRMessageProtocol> message = nil;
688
            if ([object conformsToProtocol:@protocol(OTRMessageProtocol)]) {
689
                message = (id<OTRMessageProtocol>)object;
690
            } else {
691
                return;
692
            }
693
            // Messages that never sent properly don't need to be duplicated client-side
694
            NSError *messageError = message.messageError;
695
            message = [message duplicateMessage];
696
            message.messageError = nil;
697
            message.messageSecurity = self.state.messageSecurity;
698
            message.messageDate = [NSDate date];
699
            [message saveWithTransaction:transaction];
700
            
701
            // We only need to re-upload failed media messages
702
            // otherwise just resend the URL directly
703
            if (message.messageMediaItemKey.length &&
704
                (!message.messageText.length || messageError)) {
705
                OTRMediaItem *mediaItem = [OTRMediaItem fetchObjectWithUniqueID:message.messageMediaItemKey transaction:transaction];
706
                [self sendMediaItem:mediaItem data:nil message:message transaction:transaction];
707
            } else {
708
                OTRYapMessageSendAction *sendingAction = [OTRYapMessageSendAction sendActionForMessage:message date:message.messageDate];
709
                [sendingAction saveWithTransaction:transaction];
710
            }
711
        }];
712
    }];
713
    return action;
714
}
715

    
716
- (nonnull UIAlertAction *)viewProfileAction {
717
    return [UIAlertAction actionWithTitle:VIEW_PROFILE_STRING() style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
718
        [self infoButtonPressed:action];
719
    }];
720
}
721

    
722
- (nonnull UIAlertAction *)cancleAction {
723
    return [UIAlertAction actionWithTitle:CANCEL_STRING()
724
                                    style:UIAlertActionStyleCancel
725
                                  handler:nil];
726
}
727

    
728
- (NSArray <UIAlertAction *>*)actionForMessage:(id<OTRMessageProtocol>)message {
729
    NSMutableArray <UIAlertAction *>*actions = [[NSMutableArray alloc] init];
730
    
731
    if (!message.isMessageIncoming) {
732
        // This is an outgoing message so we can offer to resend
733
        UIAlertAction *resendAction = [self resendOutgoingMessageActionForMessageKey:message.messageKey messageCollection:message.messageCollection readWriteDatabaseConnection:self.readWriteDatabaseConnection  title:RESEND_STRING()];
734
        [actions addObject:resendAction];
735
    }
736
    
737
    if (![message isKindOfClass:[OTRXMPPRoomMessage class]]) {
738
        [actions addObject:[self viewProfileAction]];
739
    }
740
    
741
    NSArray<UIAlertAction*> *mediaActions = [UIAlertAction actionsForMediaMessage:message sourceView:self.view viewController:self];
742
    [actions addObjectsFromArray:mediaActions];
743
    
744
    [actions addObject:[self cancleAction]];
745
    return actions;
746
}
747

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

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

    
853
- (BOOL) isGroupChat {
854
    __block OTRXMPPRoom *room = nil;
855
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
856
        room = [self roomWithTransaction:transaction];
857
    }];
858
    return (room != nil);
859
}
860

    
861
#pragma - mark Profile Button Methods
862

    
863
- (void)setupInfoButton {
864
    if ([self isGroupChat]) {
865
        UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"112-group" inBundle:[OTRAssets resourcesBundle] compatibleWithTraitCollection:nil] style:UIBarButtonItemStylePlain target:self action:@selector(didSelectOccupantsButton:)];
866
        self.navigationItem.rightBarButtonItem = barButtonItem;
867
    } else {
868
        UIButton* infoButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
869
        infoButton.accessibilityIdentifier = @"profileButton";
870
        [infoButton addTarget:self action:@selector(infoButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
871
        self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:infoButton];
872
    }
873
}
874

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

    
892
    UserProfileViewController *verify = [[UserProfileViewController alloc] initWithAccountKey:account.uniqueId connection:self.readOnlyDatabaseConnection form:form];
893
    verify.completionBlock = ^{
894
        [self updateEncryptionState];
895
    };
896
    UINavigationController *verifyNav = [[UINavigationController alloc] initWithRootViewController:verify];
897
    verifyNav.modalPresentationStyle = UIModalPresentationFormSheet;
898
    [self presentViewController:verifyNav animated:YES completion:nil];
899
}
900

    
901
- (void)didSelectOccupantsButton:(id)sender {
902
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"OTRRoomOccupants" bundle:[OTRAssets resourcesBundle]];
903
    OTRRoomOccupantsViewController *occupantsVC = [storyboard instantiateViewControllerWithIdentifier:@"roomOccupants"];
904
    occupantsVC.delegate = self;
905
    [occupantsVC setupViewHandlerWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection roomKey:self.threadKey];
906
    [self.navigationController pushViewController:occupantsVC animated:YES];
907
}
908

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

    
929
- (UIBarButtonItem *)rightBarButtonItem
930
{
931
    if (!self.lockBarButtonItem) {
932
        self.lockBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self.lockButton];
933
    }
934
    return self.lockBarButtonItem;
935
}
936

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

    
989
- (void)setupAccessoryButtonsWithMessageState:(OTRKitMessageState)messageState buddyStatus:(OTRThreadStatus)status textViewHasText:(BOOL)hasText
990
{
991
    self.inputToolbar.contentView.rightBarButtonItem = self.sendButton;
992
    self.inputToolbar.sendButtonLocation = JSQMessagesInputSendButtonLocationRight;
993
    self.inputToolbar.contentView.leftBarButtonItem = nil;
994
}
995

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

    
1022
#pragma - mark  dropDown Methods
1023

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

    
1052
- (void)hideDropdownAnimated:(BOOL)animated completion:(void (^)(void))completion
1053
{
1054
    if (!self.buttonDropdownView) {
1055
        if (completion) {
1056
            completion();
1057
        }
1058
    }
1059
    else {
1060
        NSTimeInterval duration = 0.3;
1061
        if (!animated) {
1062
            duration = 0.0;
1063
        }
1064
        
1065
        [UIView animateWithDuration:duration animations:^{
1066
            CGFloat height = self.buttonDropdownView.frame.size.height;
1067
            self.buttonDropdownView.topLayoutConstraint.constant = height*-1;
1068
            [self.buttonDropdownView layoutIfNeeded];
1069
            
1070
        } completion:^(BOOL finished) {
1071
            if (finished) {
1072
                [self.buttonDropdownView removeFromSuperview];
1073
                self.buttonDropdownView = nil;
1074
            }
1075
            
1076
            if (completion) {
1077
                completion();
1078
            }
1079
        }];
1080
    }
1081
}
1082

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

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

    
1131
- (id <OTRMessageProtocol,JSQMessageData>)messageAtIndexPath:(NSIndexPath *)indexPath
1132
{
1133
    // Multiple invocations with the same indexPath tend to come in groups, no need to hit the DB each time.
1134
    // Even though the object is cached, the row ID calculation still takes time
1135
    if (![indexPath isEqual:self.currentIndexPath]) {
1136
        self.currentIndexPath = indexPath;
1137
        self.currentMessage = [self.viewHandler object:indexPath];
1138
    }
1139
    return self.currentMessage;
1140
}
1141

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

    
1186
- (BOOL)showDateAtIndexPath:(NSIndexPath *)indexPath
1187
{
1188
    BOOL showDate = NO;
1189
    if (indexPath.row == 0) {
1190
        showDate = YES;
1191
    }
1192
    else {
1193
        id <OTRMessageProtocol> currentMessage = [self messageAtIndexPath:indexPath];
1194
        id <OTRMessageProtocol> previousMessage = [self messageAtIndexPath:[NSIndexPath indexPathForItem:indexPath.row-1 inSection:indexPath.section]];
1195
        
1196
        NSTimeInterval timeDifference = [[currentMessage messageDate] timeIntervalSinceDate:[previousMessage messageDate]];
1197
        if (timeDifference > kOTRMessageSentDateShowTimeInterval) {
1198
            showDate = YES;
1199
        }
1200
    }
1201
    return showDate;
1202
}
1203

    
1204
- (BOOL)showSenderDisplayNameAtIndexPath:(NSIndexPath *)indexPath {
1205
    id<OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1206
    
1207
    if(![self.threadCollection isEqualToString:[OTRXMPPRoom collection]]) {
1208
        return NO;
1209
    }
1210
    
1211
    if ([[message senderId] isEqualToString:self.senderId]) {
1212
        return NO;
1213
    }
1214
    
1215
    if(indexPath.row -1 >= 0) {
1216
        NSIndexPath *previousIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:indexPath.section];
1217
        id<OTRMessageProtocol,JSQMessageData> previousMessage = [self messageAtIndexPath:previousIndexPath];
1218
        if ([[previousMessage senderId] isEqualToString:message.senderId]) {
1219
            return NO;
1220
        }
1221
    }
1222
    
1223
    return YES;
1224
}
1225

    
1226
- (BOOL)isPushMessageAtIndexPath:(NSIndexPath *)indexPath {
1227
    id message = [self messageAtIndexPath:indexPath];
1228
    return [message isKindOfClass:[PushMessage class]];
1229
}
1230

    
1231
- (void)receivedTextViewChangedNotification:(NSNotification *)notification
1232
{
1233
    //Check if the text state changes from having some text to some or vice versa
1234
    UITextView *textView = notification.object;
1235
    [self receivedTextViewChanged:textView];
1236
}
1237

    
1238
- (void)receivedTextViewChanged:(UITextView *)textView {
1239
    BOOL hasText = [textView.text length] > 0;
1240
    if(hasText != self.state.hasText) {
1241
        self.state.hasText = hasText;
1242
        [self didUpdateState];
1243
    }
1244
    
1245
    //Everytime the textview has text and a notification comes through we are 'typing' otherwise we are done typing
1246
    if (hasText) {
1247
        [self isTyping];
1248
    } else {
1249
        [self didFinishTyping];
1250
    }
1251
    
1252
    return;
1253

    
1254
}
1255

    
1256
#pragma - mark Update UI
1257

    
1258
- (void)didUpdateState {
1259
    
1260
}
1261

    
1262
- (void)isTyping {
1263
    
1264
}
1265

    
1266
- (void)didFinishTyping {
1267
    
1268
}
1269

    
1270
#pragma - mark Sending Media Items
1271

    
1272
- (void)sendMediaItem:(OTRMediaItem *)mediaItem data:(NSData *)data message:(id<OTRMessageProtocol>)message transaction:(YapDatabaseReadWriteTransaction *)transaction
1273
{
1274
    id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction];
1275
    OTRXMPPManager *xmpp = [self xmppManagerWithTransaction:transaction];
1276
    if (!message || !thread || !xmpp) {
1277
        DDLogError(@"Error sending file due to bad paramters");
1278
        return;
1279
    }
1280
    if (data) {
1281
        thread.lastMessageIdentifier = message.messageKey;
1282
        [thread saveWithTransaction:transaction];
1283
    }
1284
    // XEP-0363
1285
    [xmpp.fileTransferManager sendWithMediaItem:mediaItem prefetchedData:data message:message];
1286
    
1287
    [mediaItem touchParentMessageWithTransaction:transaction];
1288
}
1289

    
1290
#pragma - mark Media Display Methods
1291

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

    
1315
- (void)showVideo:(OTRVideoItem *)videoItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath
1316
{
1317
    if (videoItem.filename) {
1318
        NSURL *videoURL = [[OTRMediaServer sharedInstance] urlForMediaItem:videoItem buddyUniqueId:self.threadKey];
1319
        MPMoviePlayerViewController *moviePlayerViewController = [[MPMoviePlayerViewController alloc] initWithContentURL:videoURL];
1320
        [self presentViewController:moviePlayerViewController animated:YES completion:nil];
1321
    }
1322
}
1323

    
1324
- (void)playOrPauseAudio:(OTRAudioItem *)audioItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath
1325
{
1326
    NSError *error = nil;
1327
    if  ([audioItem.uniqueId isEqualToString:self.audioPlaybackController.currentAudioItem.uniqueId]) {
1328
        if  ([self.audioPlaybackController isPlaying]) {
1329
            [self.audioPlaybackController pauseCurrentlyPlaying];
1330
        }
1331
        else {
1332
            [self.audioPlaybackController resumeCurrentlyPlaying];
1333
        }
1334
    }
1335
    else {
1336
        [self.audioPlaybackController stopCurrentlyPlaying];
1337
        OTRAudioControlsView *audioControls = [self audioControllsfromCollectionView:collectionView atIndexPath:indexPath];
1338
        [self.audioPlaybackController attachAudioControlsView:audioControls];
1339
        [self.audioPlaybackController playAudioItem:audioItem buddyUniqueId:self.threadKey error:&error];
1340
    }
1341
    
1342
    if (error) {
1343
         DDLogError(@"Audio Playback Error: %@",error);
1344
    }
1345
   
1346
}
1347

    
1348
- (OTRAudioControlsView *)audioControllsfromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath {
1349
    UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
1350
    if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
1351
        UIView *mediaView = ((JSQMessagesCollectionViewCell *)cell).mediaView;
1352
        UIView *view = [mediaView viewWithTag:kOTRAudioControlsViewTag];
1353
        if ([view isKindOfClass:[OTRAudioControlsView class]]) {
1354
            return (OTRAudioControlsView *)view;
1355
        }
1356
    }
1357
    
1358
    return nil;
1359
}
1360

    
1361
#pragma MARK - OTRMessagesCollectionViewFlowLayoutSizeProtocol methods
1362

    
1363
- (BOOL)hasBubbleSizeForCellAtIndexPath:(NSIndexPath *)indexPath {
1364
    return ![self isPushMessageAtIndexPath:indexPath];
1365
}
1366

    
1367
#pragma mark - JSQMessagesViewController method overrides
1368

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

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

    
1416
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
1417
{
1418
    if (action == @selector(delete:)) {
1419
        return YES;
1420
    }
1421
    
1422
    return [super collectionView:collectionView canPerformAction:action forItemAtIndexPath:indexPath withSender:sender];
1423
}
1424

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

    
1451
- (void)didPressAccessoryButton:(UIButton *)sender
1452
{
1453
    if ([sender isEqual:self.cameraButton]) {
1454
        [self.attachmentPicker showAlertControllerFromSourceView:sender withCompletion:nil];
1455
    }
1456
}
1457

    
1458
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
1459
{
1460
    if (action == @selector(delete:)) {
1461
        [self deleteMessageAtIndexPath:indexPath];
1462
    }
1463
    else {
1464
        [super collectionView:collectionView performAction:action forItemAtIndexPath:indexPath withSender:sender];
1465
    }
1466
}
1467

    
1468
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
1469
{
1470
    id <OTRMessageProtocol, JSQMessageData> message = [self messageAtIndexPath:indexPath];
1471

    
1472
    NSNumber *key = @(message.messageHash);
1473
    NSValue *sizeValue = [self.messageSizeCache objectForKey:key];
1474
    if (sizeValue != nil) {
1475
        return [sizeValue CGSizeValue];
1476
    }
1477

    
1478
    // Although JSQMessagesBubblesSizeCalculator has its own cache, its size is fixed and quite small, so it quickly chokes on scrolling into the past
1479
    CGSize size = [super collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:indexPath];
1480
    // The height of the first cell might change: on loading additional messages the date label most likely will disappear
1481
    if (indexPath.row > 0) {
1482
        [self.messageSizeCache setObject:[NSValue valueWithCGSize:size] forKey:key];
1483
    }
1484
    return size;
1485
}
1486

    
1487
#pragma - mark UIPopoverPresentationControllerDelegate Methods
1488

    
1489
- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController {
1490
    // Without setting this, there will be a crash on iPad
1491
    // This delegate is set in the OTRAttachmentPicker
1492
    popoverPresentationController.sourceView = self.cameraButton;
1493
}
1494

    
1495
- (void)sendPhoto:(UIImage *)photo asJPEG:(BOOL)asJPEG shouldResize:(BOOL)shouldResize {
1496
    NSParameterAssert(photo);
1497
    if (!photo) { return; }
1498
    __block OTRXMPPManager *xmpp = nil;
1499
    __block id<OTRThreadOwner> thread = nil;
1500
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1501
        xmpp = [self xmppManagerWithTransaction:transaction];
1502
        thread = [self threadObjectWithTransaction:transaction];
1503
    }];
1504
    NSParameterAssert(xmpp);
1505
    NSParameterAssert(thread);
1506
    if (!xmpp || !thread) { return; }
1507

    
1508
    [xmpp.fileTransferManager sendWithImage:photo thread:thread];
1509
}
1510

    
1511
#pragma - mark OTRAttachmentPickerDelegate Methods
1512

    
1513
- (void)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker gotPhoto:(UIImage *)photo withInfo:(NSDictionary *)info
1514
{
1515
    [self sendPhoto:photo asJPEG:YES shouldResize:YES];
1516
}
1517

    
1518
- (void)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker gotVideoURL:(NSURL *)videoURL
1519
{
1520
    if (!videoURL) { return; }
1521
    __block OTRXMPPManager *xmpp = nil;
1522
    __block id<OTRThreadOwner> thread = nil;
1523
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1524
        xmpp = [self xmppManagerWithTransaction:transaction];
1525
        thread = [self threadObjectWithTransaction:transaction];
1526
    }];
1527
    NSParameterAssert(xmpp);
1528
    NSParameterAssert(thread);
1529
    if (!xmpp || !thread) { return; }
1530

    
1531
    [xmpp.fileTransferManager sendWithVideoURL:videoURL thread:thread];
1532
}
1533

    
1534
- (NSArray <NSString *>*)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker preferredMediaTypesForSource:(UIImagePickerControllerSourceType)source
1535
{
1536
    return @[(NSString*)kUTTypeImage];
1537
}
1538

    
1539
- (void)sendAudioFileURL:(NSURL *)url
1540
{
1541
    if (!url) { return; }
1542
    __block OTRXMPPManager *xmpp = nil;
1543
    __block id<OTRThreadOwner> thread = nil;
1544
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1545
        xmpp = [self xmppManagerWithTransaction:transaction];
1546
        thread = [self threadObjectWithTransaction:transaction];
1547
    }];
1548
    NSParameterAssert(xmpp);
1549
    NSParameterAssert(thread);
1550
    if (!xmpp || !thread) { return; }
1551
    
1552
    [xmpp.fileTransferManager sendWithAudioURL:url thread:thread];
1553
}
1554

    
1555
- (void)sendImageFilePath:(NSString *)filePath asJPEG:(BOOL)asJPEG shouldResize:(BOOL)shouldResize
1556
{
1557
    [self sendPhoto:[UIImage imageWithContentsOfFile:filePath] asJPEG:asJPEG shouldResize:shouldResize];
1558
}
1559

    
1560

    
1561
#pragma - mark UIScrollViewDelegate Methods
1562

    
1563
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
1564
{
1565
    [self hideDropdownAnimated:YES completion:nil];
1566
}
1567

    
1568
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
1569
{
1570
    if (!self.loadingMessages) {
1571
        UIEdgeInsets insets = scrollView.contentInset;
1572
        CGFloat highestOffset = -insets.top;
1573
        CGFloat lowestOffset = scrollView.contentSize.height - scrollView.frame.size.height + insets.bottom;
1574
        CGFloat pos = scrollView.contentOffset.y;
1575

    
1576
        if (self.showLoadEarlierMessagesHeader && (pos == highestOffset || (pos < 0 && (scrollView.isDecelerating || scrollView.isDragging)))) {
1577
            [self updateRangeOptions:NO];
1578
        } else if (pos == lowestOffset) {
1579
            [self updateRangeOptions:YES];
1580
        }
1581
    }
1582
}
1583

    
1584
#pragma mark - UICollectionView DataSource
1585
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
1586
{
1587
    NSInteger numberOfMessages = [self.viewHandler.mappings numberOfItemsInSection:section];
1588
    return numberOfMessages;
1589
}
1590

    
1591
#pragma - mark JSQMessagesCollectionViewDataSource Methods
1592

    
1593
- (NSString *)senderDisplayName
1594
{
1595
    __block OTRAccount *account = nil;
1596
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1597
        account = [self accountWithTransaction:transaction];
1598
    }];
1599
    
1600
    NSString *senderDisplayName = @"";
1601
    if (account) {
1602
        if ([account.displayName length]) {
1603
            senderDisplayName = account.displayName;
1604
        } else {
1605
            senderDisplayName = account.username;
1606
        }
1607
    }
1608
    
1609
    return senderDisplayName;
1610
}
1611

    
1612
- (id<JSQMessageData>)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1613
{
1614
    return (id <JSQMessageData>)[self messageAtIndexPath:indexPath];
1615
}
1616

    
1617
- (id<JSQMessageBubbleImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1618
{
1619
    id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1620
    JSQMessagesBubbleImage *image = nil;
1621
    if ([message isMessageIncoming]) {
1622
        image = self.incomingBubbleImage;
1623
    }
1624
    else {
1625
        image = self.outgoingBubbleImage;
1626
    }
1627
    return image;
1628
}
1629

    
1630
- (id <JSQMessageAvatarImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
1631
{
1632
    id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath];
1633
    if ([message isKindOfClass:[PushMessage class]]) {
1634
        return nil;
1635
    }
1636
    
1637
    if (!message.isMessageIncoming) {
1638
        return [self accountAvatarImage];
1639
    }
1640
    
1641
    NSError *messageError = [message messageError];
1642
    if ((messageError && !messageError.isAutomaticDownloadError) ||
1643
        ![self isMessageTrusted:message]) {
1644
        return [self warningAvatarImage];
1645
    }
1646
    
1647
    if ([message isKindOfClass:[OTRXMPPRoomMessage class]]) {
1648
        OTRXMPPRoomMessage *roomMessage = (OTRXMPPRoomMessage *)message;
1649
        __block OTRXMPPRoomOccupant *roomOccupant = nil;
1650
        __block OTRXMPPBuddy *roomOccupantBuddy = nil;
1651
        [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1652
            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];
1653
            if (roomOccupant != nil) {
1654
                roomOccupantBuddy = [roomOccupant buddyWith:transaction];
1655
            }
1656
        }];
1657
        UIImage *avatarImage = nil;
1658
        if (roomOccupant) {
1659
            if (roomOccupantBuddy != nil) {
1660
                avatarImage = [roomOccupantBuddy avatarImage];
1661
            }
1662
            if (!avatarImage) {
1663
                avatarImage = [roomOccupant avatarImage];
1664
            }
1665
        } else if (roomMessage.senderJID) {
1666
            avatarImage = [OTRImages avatarImageWithUsername:[[XMPPJID jidWithString:roomMessage.senderJID] resource]];
1667
        } else {
1668
            return nil;
1669
        }
1670
        if (avatarImage) {
1671
            NSUInteger diameter = MIN(avatarImage.size.width, avatarImage.size.height);
1672
            return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1673
        }
1674
    }
1675
    
1676
    if ([message isMessageIncoming]) {
1677
        return [self buddyAvatarImage];
1678
    }
1679

    
1680
    return [self accountAvatarImage];
1681
}
1682

    
1683
- (JSQMessagesAvatarImage *)createAvatarImage:(UIImage *(^)(YapDatabaseReadTransaction *))getImage
1684
{
1685
    __block UIImage *avatarImage;
1686
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1687
        avatarImage = getImage(transaction);
1688
    }];
1689
    if (avatarImage != nil) {
1690
        NSUInteger diameter = (NSUInteger) MIN(avatarImage.size.width, avatarImage.size.height);
1691
        return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1692
    }
1693
    return nil;
1694
}
1695

    
1696
- (JSQMessagesAvatarImage *)warningAvatarImage
1697
{
1698
    if (_warningAvatarImage == nil) {
1699
        _warningAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1700
            return [OTRImages circleWarningWithColor:[OTRColors warnColor]];
1701
        }];
1702
    }
1703
    return _warningAvatarImage;
1704
}
1705

    
1706
- (JSQMessagesAvatarImage *)accountAvatarImage
1707
{
1708
    if (_accountAvatarImage == nil) {
1709
        _accountAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1710
            return [[self accountWithTransaction:transaction] avatarImage];
1711
        }];
1712
    }
1713
    return _accountAvatarImage;
1714
}
1715

    
1716
- (JSQMessagesAvatarImage *)buddyAvatarImage
1717
{
1718
    if (_buddyAvatarImage == nil) {
1719
        _buddyAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1720
            return [[self buddyWithTransaction:transaction] avatarImage];
1721
        }];
1722
    }
1723
    return _buddyAvatarImage;
1724
}
1725

    
1726
////// Optional //////
1727

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

    
1753

    
1754
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
1755
{
1756
    if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
1757
        id<OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1758
        
1759
        __block NSString *displayName = nil;
1760
        if ([message isKindOfClass:[OTRXMPPRoomMessage class]]) {
1761
            OTRXMPPRoomMessage *roomMessage = (OTRXMPPRoomMessage *)message;
1762
            [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1763
                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];
1764
                if (occupant) {
1765
                    OTRXMPPBuddy *buddy = [occupant buddyWith:transaction];
1766
                    if (buddy) {
1767
                        displayName = [buddy displayName];
1768
                    } else {
1769
                        displayName = [[XMPPJID jidWithString:occupant.jid] resource];
1770
                    }
1771
                }
1772
            }];
1773
        }
1774
        if (!displayName) {
1775
            displayName = [message senderDisplayName];
1776
        }
1777
        return [[NSAttributedString alloc] initWithString:displayName];
1778
    }
1779
    
1780
    return  nil;
1781
}
1782

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

    
1807
- (nullable NSAttributedString *) encryptionStatusStringForMessage:(nonnull id<OTRMessageProtocol>)message {
1808
    NSString *lockString = nil;
1809
    if (message.messageSecurity == OTRMessageTransportSecurityOTR) {
1810
        lockString = [NSString stringWithFormat:@"%@ OTR ",[NSString fa_stringForFontAwesomeIcon:FALock]];
1811
    } else if (message.messageSecurity == OTRMessageTransportSecurityOMEMO) {
1812
        lockString = [NSString stringWithFormat:@"%@ OMEMO ",[NSString fa_stringForFontAwesomeIcon:FALock]];
1813
    }
1814
    else {
1815
        lockString = [NSString fa_stringForFontAwesomeIcon:FAUnlock];
1816
    }
1817
    UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12];
1818
    if (!font) {
1819
        font = [UIFont systemFontOfSize:12];
1820
    }
1821
    return [[NSAttributedString alloc] initWithString:lockString attributes:@{NSFontAttributeName: font}];
1822
}
1823

    
1824

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

    
1902

    
1903
#pragma - mark  JSQMessagesCollectionViewDelegateFlowLayout Methods
1904

    
1905
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1906
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1907
heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
1908
{
1909
    CGFloat height = 0.0f;
1910
    if ([self showDateAtIndexPath:indexPath]) {
1911
        height += kJSQMessagesCollectionViewCellLabelHeightDefault;
1912
    }
1913
    
1914
    if ([self isPushMessageAtIndexPath:indexPath]) {
1915
        height += kJSQMessagesCollectionViewCellLabelHeightDefault;
1916
    }
1917
    return height;
1918
}
1919

    
1920
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1921
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1922
heightForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
1923
{
1924
    if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
1925
        return kJSQMessagesCollectionViewCellLabelHeightDefault;
1926
    }
1927
    return 0.0f;
1928
}
1929

    
1930
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1931
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1932
heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
1933
{
1934
    CGFloat height = kJSQMessagesCollectionViewCellLabelHeightDefault;
1935
    if ([self isPushMessageAtIndexPath:indexPath]) {
1936
        height = 0.0f;
1937
    }
1938
    return height;
1939
}
1940

    
1941
- (void)deleteMessageAtIndexPath:(NSIndexPath *)indexPath
1942
{
1943
    __block id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1944
    __weak __typeof__(self) weakSelf = self;
1945
    [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
1946
        __typeof__(self) strongSelf = weakSelf;
1947
        [transaction removeObjectForKey:[message messageKey] inCollection:[message messageCollection]];
1948
        //Update Last message date for sorting and grouping
1949
        OTRBuddy *buddy = [[strongSelf buddyWithTransaction:transaction] copy];
1950
        buddy.lastMessageId = nil;
1951
        [buddy saveWithTransaction:transaction];
1952
    }];
1953
}
1954

    
1955
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapAvatarImageView:(UIImageView *)avatarImageView atIndexPath:(NSIndexPath *)indexPath
1956
{
1957
    id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1958
    [self didTapAvatar:message sender:avatarImageView];
1959
}
1960

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

    
2004
        [self presentViewController:alert animated:YES completion:nil];
2005
    }
2006
}
2007

    
2008
#pragma - mark database view delegate
2009

    
2010
- (void)didSetupMappings:(OTRYapViewHandler *)handler
2011
{
2012
    // The databse view is setup now so refresh from there
2013
    [self updateViewWithKey:self.threadKey collection:self.threadCollection];
2014
    [self updateRangeOptions:YES];
2015
    [self.collectionView reloadData];
2016
}
2017

    
2018
- (void)didReceiveChanges:(OTRYapViewHandler *)handler key:(NSString *)key collection:(NSString *)collection
2019
{
2020
    [self updateViewWithKey:key collection:collection];
2021
}
2022

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

    
2089
#pragma - mark UITextViewDelegateMethods
2090

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

    
2113
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
2114
    return NO;
2115
}
2116

    
2117
- (void)viewWillLayoutSubviews {
2118
    self.currentIndexPath = nil;
2119
    [super viewWillLayoutSubviews];
2120
}
2121

    
2122
- (void)viewDidLayoutSubviews {
2123
    [super viewDidLayoutSubviews];
2124
    [self layoutJIDForwardingHeader];
2125
}
2126

    
2127
#pragma - mark Buddy Migration methods
2128

    
2129
- (nullable XMPPJID *)getForwardingJIDForBuddy:(OTRXMPPBuddy *)xmppBuddy {
2130
    XMPPJID *ret = nil;
2131
    if (xmppBuddy != nil && xmppBuddy.vCardTemp != nil) {
2132
        ret = xmppBuddy.vCardTemp.jid;
2133
    }
2134
    return ret;
2135
}
2136

    
2137
- (void)layoutJIDForwardingHeader {
2138
    if (self.jidForwardingHeaderView != nil) {
2139
        [self.jidForwardingHeaderView setNeedsLayout];
2140
        [self.jidForwardingHeaderView layoutIfNeeded];
2141
        int height = [self.jidForwardingHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 1;
2142
        self.jidForwardingHeaderView.frame = CGRectMake(0, self.topLayoutGuide.length, self.view.frame.size.width, height);
2143
        [self.view bringSubviewToFront:self.jidForwardingHeaderView];
2144
        self.topContentAdditionalInset = height;
2145
    }
2146
}
2147

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

    
2176
- (void)showJIDForwardingHeaderWithNewJID:(XMPPJID *)newJid {
2177
    if (self.jidForwardingHeaderView == nil) {
2178
        UINib *nib = [UINib nibWithNibName:@"MigratedBuddyHeaderView" bundle:OTRAssets.resourcesBundle];
2179
        MigratedBuddyHeaderView *header = (MigratedBuddyHeaderView*)[nib instantiateWithOwner:self options:nil][0];
2180
        [header setForwardingJID:newJid];
2181
        [header.titleLabel setText:MIGRATED_BUDDY_STRING()];
2182
        [header.descriptionLabel setText:MIGRATED_BUDDY_INFO_STRING()];
2183
        [header.switchButton setTitle:MIGRATED_BUDDY_SWITCH() forState:UIControlStateNormal];
2184
        [header.ignoreButton setTitle:MIGRATED_BUDDY_IGNORE() forState:UIControlStateNormal];
2185
        [header setBackgroundColor:UIColor.whiteColor];
2186
        [self.view addSubview:header];
2187
        [self.view bringSubviewToFront:header];
2188
        self.jidForwardingHeaderView = header;
2189
        [self.view setNeedsLayout];
2190
    }
2191
}
2192

    
2193
- (IBAction)didPressMigratedIgnore {
2194
    if (self.jidForwardingHeaderView != nil) {
2195
        self.jidForwardingHeaderView.hidden = YES;
2196
        self.topContentAdditionalInset = 0;
2197
    }
2198
}
2199

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

    
2233
#pragma - mark Group chat support
2234

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

    
2253
#pragma - mark OTRRoomOccupantsViewControllerDelegate
2254

    
2255
- (void)didLeaveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
2256
    __block OTRXMPPRoom *room = nil;
2257
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2258
        room = [self roomWithTransaction:transaction];
2259
    }];
2260
    if (room) {
2261
        [self setThreadKey:nil collection:nil];
2262
        [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
2263
            [room removeWithTransaction:transaction];
2264
        }];
2265
    }
2266
    [self.navigationController popViewControllerAnimated:NO];
2267
    if ([[self.navigationController viewControllers] count] > 1) {
2268
        [self.navigationController popViewControllerAnimated:YES];
2269
    } else {
2270
        [self.navigationController.navigationController popViewControllerAnimated:YES];
2271
    }
2272
    
2273
}
2274

    
2275
- (void)didArchiveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
2276
    __block OTRXMPPRoom *room = nil;
2277
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2278
        room = [self roomWithTransaction:transaction];
2279
    }];
2280
    if (room) {
2281
        [self setThreadKey:nil collection:nil];
2282
        [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
2283
            room.isArchived = YES;
2284
            [room saveWithTransaction:transaction];
2285
        }];
2286
    }
2287
    [self.navigationController popViewControllerAnimated:NO];
2288
    if ([[self.navigationController viewControllers] count] > 1) {
2289
        [self.navigationController popViewControllerAnimated:YES];
2290
    } else {
2291
        [self.navigationController.navigationController popViewControllerAnimated:YES];
2292
    }
2293
}
2294
@end