Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / View Controllers / OTRMessagesViewController.m @ 8d76e2e3

History | View | Annotate | Download (96.4 KB)

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

    
9
#import "OTRMessagesViewController.h"
10

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

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

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

    
60
@import AVFoundation;
61
@import MediaPlayer;
62

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

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

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

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

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

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

    
88

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

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

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

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

    
105
@end
106

    
107
@implementation OTRMessagesViewController
108

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

    
122
#pragma - mark Lifecylce Methods
123

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

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

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

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

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

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

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

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

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

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

    
313
#pragma - mark Setters & getters
314

    
315
- (void) fetchMessageHistory {
316
    [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
317
        id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction];
318
        OTRXMPPManager *xmpp = [self xmppManagerWithTransaction:transaction];
319
        [xmpp fetchHistoryForThread:thread transaction:transaction];
320
    }];
321
}
322

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
672
}
673

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

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

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

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

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

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

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

    
859
#pragma - mark Profile Button Methods
860

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

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

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

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

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

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

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

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

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

    
1020
#pragma - mark  dropDown Methods
1021

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

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

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

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

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

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

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

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

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

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

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

    
1252
}
1253

    
1254
#pragma - mark Update UI
1255

    
1256
- (void)didUpdateState {
1257
    
1258
}
1259

    
1260
- (void)isTyping {
1261
    
1262
}
1263

    
1264
- (void)didFinishTyping {
1265
    
1266
}
1267

    
1268
#pragma - mark Sending Media Items
1269

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

    
1288
#pragma - mark Media Display Methods
1289

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

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

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

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

    
1359
#pragma MARK - OTRMessagesCollectionViewFlowLayoutSizeProtocol methods
1360

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

    
1365
#pragma mark - JSQMessagesViewController method overrides
1366

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

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

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

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

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

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

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

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

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

    
1485
#pragma - mark UIPopoverPresentationControllerDelegate Methods
1486

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

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

    
1506
    [xmpp.fileTransferManager sendWithImage:photo thread:thread];
1507
}
1508

    
1509
#pragma - mark OTRAttachmentPickerDelegate Methods
1510

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

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

    
1529
    [xmpp.fileTransferManager sendWithVideoURL:videoURL thread:thread];
1530
}
1531

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

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

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

    
1558

    
1559
#pragma - mark UIScrollViewDelegate Methods
1560

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

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

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

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

    
1589
#pragma - mark JSQMessagesCollectionViewDataSource Methods
1590

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

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

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

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

    
1672
    return [self accountAvatarImage];
1673
}
1674

    
1675
- (JSQMessagesAvatarImage *)createAvatarImage:(UIImage *(^)(YapDatabaseReadTransaction *))getImage
1676
{
1677
    __block UIImage *avatarImage;
1678
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1679
        avatarImage = getImage(transaction);
1680
    }];
1681
    if (avatarImage != nil) {
1682
        NSUInteger diameter = (NSUInteger) MIN(avatarImage.size.width, avatarImage.size.height);
1683
        return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1684
    }
1685
    return nil;
1686
}
1687

    
1688
- (JSQMessagesAvatarImage *)warningAvatarImage
1689
{
1690
    if (_warningAvatarImage == nil) {
1691
        _warningAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1692
            return [OTRImages circleWarningWithColor:[OTRColors warnColor]];
1693
        }];
1694
    }
1695
    return _warningAvatarImage;
1696
}
1697

    
1698
- (JSQMessagesAvatarImage *)accountAvatarImage
1699
{
1700
    if (_accountAvatarImage == nil) {
1701
        _accountAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1702
            return [[self accountWithTransaction:transaction] avatarImage];
1703
        }];
1704
    }
1705
    return _accountAvatarImage;
1706
}
1707

    
1708
- (JSQMessagesAvatarImage *)buddyAvatarImage
1709
{
1710
    if (_buddyAvatarImage == nil) {
1711
        _buddyAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1712
            return [[self buddyWithTransaction:transaction] avatarImage];
1713
        }];
1714
    }
1715
    return _buddyAvatarImage;
1716
}
1717

    
1718
////// Optional //////
1719

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

    
1745

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

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

    
1799
- (nullable NSAttributedString *) encryptionStatusStringForMessage:(nonnull id<OTRMessageProtocol>)message {
1800
    NSString *lockString = nil;
1801
    if (message.messageSecurity == OTRMessageTransportSecurityOTR) {
1802
        lockString = [NSString stringWithFormat:@"%@ OTR ",[NSString fa_stringForFontAwesomeIcon:FALock]];
1803
    } else if (message.messageSecurity == OTRMessageTransportSecurityOMEMO) {
1804
        lockString = [NSString stringWithFormat:@"%@ OMEMO ",[NSString fa_stringForFontAwesomeIcon:FALock]];
1805
    }
1806
    else {
1807
        lockString = [NSString fa_stringForFontAwesomeIcon:FAUnlock];
1808
    }
1809
    UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12];
1810
    if (!font) {
1811
        font = [UIFont systemFontOfSize:12];
1812
    }
1813
    return [[NSAttributedString alloc] initWithString:lockString attributes:@{NSFontAttributeName: font}];
1814
}
1815

    
1816

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

    
1894

    
1895
#pragma - mark  JSQMessagesCollectionViewDelegateFlowLayout Methods
1896

    
1897
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1898
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1899
heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
1900
{
1901
    CGFloat height = 0.0f;
1902
    if ([self showDateAtIndexPath:indexPath]) {
1903
        height += kJSQMessagesCollectionViewCellLabelHeightDefault;
1904
    }
1905
    
1906
    if ([self isPushMessageAtIndexPath:indexPath]) {
1907
        height += kJSQMessagesCollectionViewCellLabelHeightDefault;
1908
    }
1909
    return height;
1910
}
1911

    
1912
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1913
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1914
heightForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath
1915
{
1916
    if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
1917
        return kJSQMessagesCollectionViewCellLabelHeightDefault;
1918
    }
1919
    return 0.0f;
1920
}
1921

    
1922
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
1923
                   layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout
1924
heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
1925
{
1926
    CGFloat height = kJSQMessagesCollectionViewCellLabelHeightDefault;
1927
    if ([self isPushMessageAtIndexPath:indexPath]) {
1928
        height = 0.0f;
1929
    }
1930
    return height;
1931
}
1932

    
1933
- (void)deleteMessageAtIndexPath:(NSIndexPath *)indexPath
1934
{
1935
    __block id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1936
    __weak __typeof__(self) weakSelf = self;
1937
    [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
1938
        __typeof__(self) strongSelf = weakSelf;
1939
        [transaction removeObjectForKey:[message messageKey] inCollection:[message messageCollection]];
1940
        //Update Last message date for sorting and grouping
1941
        OTRBuddy *buddy = [[strongSelf buddyWithTransaction:transaction] copy];
1942
        buddy.lastMessageId = nil;
1943
        [buddy saveWithTransaction:transaction];
1944
    }];
1945
}
1946

    
1947
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapAvatarImageView:(UIImageView *)avatarImageView atIndexPath:(NSIndexPath *)indexPath
1948
{
1949
    id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath];
1950
    [self didTapAvatar:message sender:avatarImageView];
1951
}
1952

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

    
1996
        [self presentViewController:alert animated:YES completion:nil];
1997
    }
1998
}
1999

    
2000
#pragma - mark database view delegate
2001

    
2002
- (void)didSetupMappings:(OTRYapViewHandler *)handler
2003
{
2004
    // The databse view is setup now so refresh from there
2005
    [self updateViewWithKey:self.threadKey collection:self.threadCollection];
2006
    [self updateRangeOptions:YES];
2007
    [self.collectionView reloadData];
2008
}
2009

    
2010
- (void)didReceiveChanges:(OTRYapViewHandler *)handler key:(NSString *)key collection:(NSString *)collection
2011
{
2012
    [self updateViewWithKey:key collection:collection];
2013
}
2014

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

    
2081
#pragma - mark UITextViewDelegateMethods
2082

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

    
2105
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
2106
    return NO;
2107
}
2108

    
2109
- (void)viewWillLayoutSubviews {
2110
    self.currentIndexPath = nil;
2111
    [super viewWillLayoutSubviews];
2112
}
2113

    
2114
- (void)viewDidLayoutSubviews {
2115
    [super viewDidLayoutSubviews];
2116
    [self layoutJIDForwardingHeader];
2117
}
2118

    
2119
#pragma - mark Buddy Migration methods
2120

    
2121
- (nullable XMPPJID *)getForwardingJIDForBuddy:(OTRXMPPBuddy *)xmppBuddy {
2122
    XMPPJID *ret = nil;
2123
    if (xmppBuddy != nil && xmppBuddy.vCardTemp != nil) {
2124
        ret = xmppBuddy.vCardTemp.jid;
2125
    }
2126
    return ret;
2127
}
2128

    
2129
- (void)layoutJIDForwardingHeader {
2130
    if (self.jidForwardingHeaderView != nil) {
2131
        [self.jidForwardingHeaderView setNeedsLayout];
2132
        [self.jidForwardingHeaderView layoutIfNeeded];
2133
        int height = [self.jidForwardingHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 1;
2134
        self.jidForwardingHeaderView.frame = CGRectMake(0, self.topLayoutGuide.length, self.view.frame.size.width, height);
2135
        [self.view bringSubviewToFront:self.jidForwardingHeaderView];
2136
        self.topContentAdditionalInset = height;
2137
    }
2138
}
2139

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

    
2168
- (void)showJIDForwardingHeaderWithNewJID:(XMPPJID *)newJid {
2169
    if (self.jidForwardingHeaderView == nil) {
2170
        UINib *nib = [UINib nibWithNibName:@"MigratedBuddyHeaderView" bundle:OTRAssets.resourcesBundle];
2171
        MigratedBuddyHeaderView *header = (MigratedBuddyHeaderView*)[nib instantiateWithOwner:self options:nil][0];
2172
        [header setForwardingJID:newJid];
2173
        [header.titleLabel setText:MIGRATED_BUDDY_STRING()];
2174
        [header.descriptionLabel setText:MIGRATED_BUDDY_INFO_STRING()];
2175
        [header.switchButton setTitle:MIGRATED_BUDDY_SWITCH() forState:UIControlStateNormal];
2176
        [header.ignoreButton setTitle:MIGRATED_BUDDY_IGNORE() forState:UIControlStateNormal];
2177
        [header setBackgroundColor:UIColor.whiteColor];
2178
        [self.view addSubview:header];
2179
        [self.view bringSubviewToFront:header];
2180
        self.jidForwardingHeaderView = header;
2181
        [self.view setNeedsLayout];
2182
    }
2183
}
2184

    
2185
- (IBAction)didPressMigratedIgnore {
2186
    if (self.jidForwardingHeaderView != nil) {
2187
        self.jidForwardingHeaderView.hidden = YES;
2188
        self.topContentAdditionalInset = 0;
2189
    }
2190
}
2191

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

    
2225
#pragma - mark Group chat support
2226

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

    
2245
#pragma - mark OTRRoomOccupantsViewControllerDelegate
2246

    
2247
- (void)didLeaveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
2248
    __block OTRXMPPRoom *room = nil;
2249
    [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
2250
        room = [self roomWithTransaction:transaction];
2251
    }];
2252
    if (room) {
2253
        [self setThreadKey:nil collection:nil];
2254
        [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
2255
            [room removeWithTransaction:transaction];
2256
        }];
2257
    }
2258
    [self.navigationController popViewControllerAnimated:NO];
2259
    if ([[self.navigationController viewControllers] count] > 1) {
2260
        [self.navigationController popViewControllerAnimated:YES];
2261
    } else {
2262
        [self.navigationController.navigationController popViewControllerAnimated:YES];
2263
    }
2264
    
2265
}
2266

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