MessagesVC.mm 41.1 KB
Newer Older
1

2
/*
3
 *  Copyright (C) 2015-2018 Savoir-faire Linux Inc.
4
 *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
5
 *          Anthony Léonard <anthony.leonard@savoirfairelinux.com>
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
 */

#import <QPixmap>
#import <QtMacExtras/qmacfunctions.h>

25
// LRC
26
#import <globalinstances.h>
27
#import <api/interaction.h>
28 29 30 31

#import "MessagesVC.h"
#import "views/IMTableCellView.h"
#import "views/MessageBubbleView.h"
32
#import "views/NSImage+Extensions.h"
33
#import "delegates/ImageManipulationDelegate.h"
34
#import "utils.h"
35
#import "views/NSColor+RingTheme.h"
36 37 38
#import "views/IconButton.h"
#import <QuickLook/QuickLook.h>
#import <Quartz/Quartz.h>
39

40

41
@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource> {
42

43
    __unsafe_unretained IBOutlet NSTableView* conversationView;
44
    __unsafe_unretained IBOutlet NSView* containerView;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
45 46 47 48
    __unsafe_unretained IBOutlet NSTextField* messageField;
    __unsafe_unretained IBOutlet IconButton *sendFileButton;
    __unsafe_unretained IBOutlet NSLayoutConstraint* sendPanelHeight;
    __unsafe_unretained IBOutlet NSLayoutConstraint* messagesBottomMargin;
49

50
    std::string convUid_;
51
    lrc::api::ConversationModel* convModel_;
52 53
    const lrc::api::conversation::Info* cachedConv_;

54
    QMetaObject::Connection newInteractionSignal_;
55 56 57 58 59

    // Both are needed to invalidate cached conversation as pointer
    // may not be referencing the same conversation anymore
    QMetaObject::Connection modelSortedSignal_;
    QMetaObject::Connection filterChangedSignal_;
60
    QMetaObject::Connection interactionStatusUpdatedSignal_;
61
    NSString* previewImage;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
62
    NSMutableDictionary *pendingMessagesToSend;
63 64 65 66 67
}


@end

68 69
// Tags for view
NSInteger const GENERIC_INT_TEXT_TAG = 100;
70 71 72 73 74 75 76 77
NSInteger const GENERIC_INT_TIME_TAG = 200;

// views size
CGFloat   const GENERIC_CELL_HEIGHT       = 60;
CGFloat   const TIME_BOX_HEIGHT           = 34;
CGFloat   const MESSAGE_TEXT_PADDING      = 10;
CGFloat   const MAX_TRANSFERED_IMAGE_SIZE = 250;
CGFloat   const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
78 79 80
NSInteger const MEESAGE_MARGIN = 21;
NSInteger const SEND_PANEL_DEFAULT_HEIGHT = 60;
NSInteger const SEND_PANEL_MAX_HEIGHT = 120;
81

82 83
@implementation MessagesVC

84

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
//MessageBuble type
typedef NS_ENUM(NSInteger, MessageSequencing) {
    SINGLE_WITH_TIME       = 0,
    SINGLE_WITHOUT_TIME    = 1,
    FIRST_WITH_TIME        = 2,
    FIRST_WITHOUT_TIME     = 3,
    MIDDLE_IN_SEQUENCE     = 5,
    LAST_IN_SEQUENCE       = 6,
};

- (void)awakeFromNib
{
    NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
    [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
    [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
    [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
    [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
    [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
103 104 105
    [[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
    [messageField setFocusRingType:NSFocusRingTypeNone];
    [conversationView setWantsLayer:YES];
106
}
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
107 108 109 110 111 112 113 114 115 116 117 118 119 120

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        pendingMessagesToSend = [[NSMutableDictionary alloc] init];
    }
    return self;
}

- (void)setMessage:(NSString *)newValue {
    _message = [newValue removeEmptyLinesAtBorders];
}

121
-(void) clearData {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
122 123 124
    if (!convUid_.empty()) {
        pendingMessagesToSend[@(convUid_.c_str())] = messageField.stringValue;
    }
125 126 127 128 129 130 131 132 133
    cachedConv_ = nil;
    convUid_ = "";
    convModel_ = nil;

    QObject::disconnect(modelSortedSignal_);
    QObject::disconnect(filterChangedSignal_);
    QObject::disconnect(interactionStatusUpdatedSignal_);
    QObject::disconnect(newInteractionSignal_);
}
134

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
135 136 137 138 139 140 141 142 143 144 145
-(void) scrollToBottom {
    CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
    NSRange range = [conversationView rowsInRect:visibleRect];
    NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
    NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
    if (([conversationView numberOfRows] > 0) &&
        lastvisibleRow == ([conversationView numberOfRows] -1)) {
        [conversationView scrollToEndOfDocument:nil];
    }
}

146 147 148 149 150 151 152 153
-(const lrc::api::conversation::Info*) getCurrentConversation
{
    if (convModel_ == nil || convUid_.empty())
        return nil;

    if (cachedConv_ != nil)
        return cachedConv_;

154
    auto it = getConversationFromUid(convUid_, *convModel_);
155
    if (it != convModel_->allFilteredConversations().end())
156
        cachedConv_ = &(*it);
157

158 159 160
    return cachedConv_;
}

161 162 163 164 165
-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update {
    auto* conv = [self getCurrentConversation];

    if (conv == nil)
        return;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
166 167 168 169 170 171
    auto it = conv->interactions.find(uid);
    if (it == conv->interactions.end()) {
        return;
    }
    auto itIndex = distance(conv->interactions.begin(),it);
    NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:itIndex];
172
    //reload previous message to update bubbleview
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
173 174 175 176 177 178 179 180
    if (itIndex > 0) {
        auto previousIt = it;
        previousIt--;
        auto previousInteraction = previousIt->second;
        if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
            NSRange range = NSMakeRange(itIndex - 1, 2);
            indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
        }
181 182 183 184 185 186
    }
    if (update) {
        [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
    }
    [conversationView reloadDataForRowIndexes: indexSet
                                columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
187 188 189 190 191 192
    CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
    NSRange range = [conversationView rowsInRect:visibleRect];
    NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
    NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
    if (([conversationView numberOfRows] > 0) &&
        lastvisibleRow == ([conversationView numberOfRows] -1)) {
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
        [conversationView scrollToEndOfDocument:nil];
    }
}

-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update updateConversation:(bool) updateConversation {
    auto* conv = [self getCurrentConversation];

    if (conv == nil)
        return;
    auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
    NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
    //reload previous message to update bubbleview
    if (it > 0) {
        NSRange range = NSMakeRange(it - 1, it);
        indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
    }
    if (update) {
        [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
    }
    [conversationView reloadDataForRowIndexes: indexSet
                                columnIndexes:[NSIndexSet indexSetWithIndex:0]];
    if (update) {
        [conversationView scrollToEndOfDocument:nil];
    }
}

219
-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
220 221 222 223 224 225 226 227
{
    if (convUid_ == convUid && convModel_ == model)
        return;

    cachedConv_ = nil;
    convUid_ = convUid;
    convModel_ = model;

228 229 230 231
    // Signal triggered when messages are received or their status updated
    QObject::disconnect(newInteractionSignal_);
    QObject::disconnect(interactionStatusUpdatedSignal_);
    newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
232 233 234 235 236 237 238
                                             [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
                                                 if (uid != convUid_)
                                                     return;
                                                 cachedConv_ = nil;
                                                 [conversationView noteNumberOfRowsChanged];
                                                 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
                                             });
239 240 241 242 243
    interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
                                                       [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
                                                           if (uid != convUid_)
                                                               return;
                                                           cachedConv_ = nil;
244 245 246 247 248
                                                           bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
                                                           if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
                                                               convModel_->refreshFilter();
                                                           }
                                                           [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
249
                                                       });
250 251 252 253 254 255 256 257 258 259 260 261 262

    // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
    // after a reordering.
    QObject::disconnect(modelSortedSignal_);
    QObject::disconnect(filterChangedSignal_);
    modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
                                          [self](){
                                              cachedConv_ = nil;
                                          });
    filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
                                          [self](){
                                              cachedConv_ = nil;
                                          });
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
263 264 265 266 267 268 269 270 271 272 273 274
    if (pendingMessagesToSend[@(convUid_.c_str())]) {
        self.message = pendingMessagesToSend[@(convUid_.c_str())];
        [self updateSendMessageHeight];
    } else {
        self.message = @"";
        if(messagesBottomMargin.constant != SEND_PANEL_DEFAULT_HEIGHT) {
            sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
            messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
            [self scrollToBottom];
        }
    }
    conversationView.alphaValue = 0.0;
275
    [conversationView reloadData];
276
    [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
277 278 279 280 281 282 283 284 285 286 287 288
    CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
    fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
    fadeIn.toValue = [NSNumber numberWithFloat:1.0];
    fadeIn.duration = 0.4f;

    [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
    conversationView.alphaValue = 1;
    auto* conv = [self getCurrentConversation];

    if (conv == nil)
        return;
    [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
289 290
}

291
#pragma mark - configure cells
292

293
-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
294 295 296
{
    NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
    NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
297
    NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
298 299 300 301

    // TODO: Fix symbol in LRC
    NSString* fixedString = [text stringByReplacingOccurrencesOfString:@"🕽" withString:@"📞"];
    [textField setStringValue:fixedString];
302
    [timeField setStringValue:time];
303 304 305 306

    return result;
}

307
-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
308 309 310
{
    IMTableCellView* result;

311 312 313 314 315
    auto type = interaction.type;
    auto status = interaction.status;

    NSString* fileName = @"incoming file";

316 317 318 319
    // First, view is created
    if (type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
        switch (status) {
            case lrc::api::interaction::Status::TRANSFER_CREATED:
320 321 322 323 324 325 326
            case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
                result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
                [result.acceptButton setAction:@selector(acceptIncomingFile:)];
                [result.acceptButton setTarget:self];
                [result.declineButton setAction:@selector(declineIncomingFile:)];
                [result.declineButton setTarget:self];
                break;}
327
            case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
328 329 330 331 332 333
            case lrc::api::interaction::Status::TRANSFER_ONGOING: {
                result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
                [result.progressIndicator startAnimation:conversationView];
                [result.declineButton setAction:@selector(declineIncomingFile:)];
                [result.declineButton setTarget:self];
                break;}
334
            case lrc::api::interaction::Status::TRANSFER_FINISHED:
335 336 337 338 339
                result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
                [result.transferedFileName setAction:@selector(imagePreview:)];
                [result.transferedFileName setTarget:self];
                [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
                break;
340 341
            case lrc::api::interaction::Status::TRANSFER_CANCELED:
            case lrc::api::interaction::Status::TRANSFER_ERROR:
342 343
                result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
                break;
344 345
        }
    } else if (type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
346
        NSString* fileName = @"sent file";
347 348 349
        switch (status) {
            case lrc::api::interaction::Status::TRANSFER_CREATED:
            case lrc::api::interaction::Status::TRANSFER_ONGOING:
350
            case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
351
            case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
352
                result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
353
                [result.progressIndicator startAnimation:nil];
354 355
                [result.declineButton setAction:@selector(declineIncomingFile:)];
                [result.declineButton setTarget:self];
356 357
                break;
            case lrc::api::interaction::Status::TRANSFER_FINISHED:
358 359 360 361 362
                result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
                [result.transferedFileName setAction:@selector(imagePreview:)];
                [result.transferedFileName setTarget:self];
                [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
                break;
363 364
            case lrc::api::interaction::Status::TRANSFER_CANCELED:
            case lrc::api::interaction::Status::TRANSFER_ERROR:
365
            case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
366
                result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
367 368 369 370 371
        }
    }

    // Then status label is updated if needed
    switch (status) {
372
            [result.statusLabel setTextColor:[NSColor textColor]];
373
        case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
374
            [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
375
            [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
376 377
            break;
        case lrc::api::interaction::Status::TRANSFER_CANCELED:
378
            [result.statusLabel setTextColor:[NSColor orangeColor]];
379
            [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
380 381
            break;
        case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
382
            [result.statusLabel setTextColor:[NSColor errorTransferColor]];
383
            [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
384 385
            break;
        case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
386
             [result.statusLabel setTextColor:[NSColor textColor]];
387 388
            [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
            break;
389
    }
390
    result.transferedImage.image = nil;
391
    [result.openImagebutton setHidden:YES];
392 393 394 395
    [result.msgBackground setHidden:NO];
    [result invalidateImageConstraints];
    NSString* name =  @(interaction.body.c_str());
    if (name.length > 0) {
396
       fileName = [name lastPathComponent];
397
    }
398 399 400 401 402 403 404 405 406 407
    NSFont *nameFont = [NSFont userFontOfSize:14.0];
    NSColor *nameColor = [NSColor textColor];
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
    paragraphStyle.alignment = NSTextAlignmentLeft;
    NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
                                 nameColor,NSForegroundColorAttributeName,
                                 paragraphStyle,NSParagraphStyleAttributeName, nil];
    NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
    result.transferedFileName.attributedTitle = nameAttributedString;
408
    if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
409 410 411 412 413 414
        NSColor *higlightColor = [NSColor grayColor];
        NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
                                  higlightColor,NSForegroundColorAttributeName,
                                  paragraphStyle,NSParagraphStyleAttributeName, nil];
        NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
        result.transferedFileName.attributedAlternateTitle = alternativeString;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
415
        NSImage* image = [self getImageForFilePath:name];
416
        if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
417
            image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
418 419
        }
        if(image != nil) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
420 421 422 423
            result.transferedImage.image = image;
            [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
            [result.openImagebutton setAction:@selector(imagePreview:)];
            [result.openImagebutton setTarget:self];
424
            [result.openImagebutton setHidden:NO];
425 426 427 428 429 430 431 432 433 434 435 436
        }
    }
    [result setupForInteraction:interactionID];
    NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
    NSString* timeString = [self timeForMessage: msgTime];
    result.timeLabel.stringValue = timeString;
    bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
    if (!isOutgoing) {
        auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
        auto* conv = [self getCurrentConversation];
        [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
    }
437 438 439
    return result;
}

440 441
#pragma mark - NSTableViewDelegate methods
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
442 443 444 445
{
    return YES;
}

446
- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
447
{
448
    return NO;
449 450
}

451
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
452
{
453
    
454 455 456 457 458
    auto* conv = [self getCurrentConversation];

    if (conv == nil)
        return nil;

459
    auto it = conv->interactions.begin();
460

461
    std::advance(it, row);
462

463
    IMTableCellView* result;
464
    auto interaction = it->second;
465 466
    bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);

467 468 469 470 471 472 473 474 475 476
    switch (interaction.type) {
        case lrc::api::interaction::Type::TEXT:
            if (isOutgoing) {
                result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
            } else {
                result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
            }
            break;
        case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER:
        case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER:
477
            return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
478
            break;
479
        case lrc::api::interaction::Type::CONTACT:
480 481 482 483 484
        case lrc::api::interaction::Type::CALL: {
            NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
            NSString* timeString = [self timeForMessage: msgTime];
            return [self makeGenericInteractionViewForTableView:tableView withText:@(interaction.body.c_str()) andTime:timeString];
        }
485 486
        default:  // If interaction is not of a known type
            return nil;
487
    }
488 489 490 491 492 493 494 495 496 497 498 499
    MessageSequencing sequence = [self computeSequencingFor:row];
    BubbleType type = SINGLE;
    if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
        type = FIRST;
    }
    if (sequence == MIDDLE_IN_SEQUENCE) {
        type = MIDDLE;
    }
    if (sequence == LAST_IN_SEQUENCE) {
        type = LAST;
    }
    result.msgBackground.type = type;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
    bool sendingFail = false;
    [result.messageStatus setHidden:YES];
    if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
        if (interaction.status == lrc::api::interaction::Status::SENDING) {
            [result.messageStatus setHidden:NO];
            [result.sendingMessageIndicator startAnimation:nil];
            [result.messageFailed setHidden:YES];
        } else if (interaction.status == lrc::api::interaction::Status::FAILED) {
            [result.messageStatus setHidden:NO];
            [result.sendingMessageIndicator setHidden:YES];
            [result.messageFailed setHidden:NO];
            sendingFail = true;
        }
    }
    [result setupForInteraction:it->first isFailed: sendingFail];
515
    bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
516
    bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
517 518 519
    [result.msgBackground setNeedsDisplay:YES];
    [result setNeedsDisplay:YES];
    [result.timeBox setNeedsDisplay:YES];
520

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
521 522 523
    NSString *text = @(interaction.body.c_str());
    text = [text removeEmptyLinesAtBorders];

524
    NSMutableAttributedString* msgAttString =
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
525
    [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:text]
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
526
                                           attributes:[self messageAttributes]];
527

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
528
    CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
529

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
530
    [result updateMessageConstraint:messageSize.width  andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
531
    [[result.msgView textStorage] appendAttributedString:msgAttString];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
532
   // [result.msgView checkTextInDocument:nil];
533

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
    NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
    NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];

    [result.msgView.textStorage beginEditing];

    for (NSTextCheckingResult *match in matches) {
        if (!match.URL) continue;

        NSDictionary *linkAttributes = @{
                                         NSLinkAttributeName: match.URL,
                                         };
        [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
    }

    [result.msgView.textStorage endEditing];

550 551 552 553
    if (shouldDisplayTime) {
        NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
        NSString* timeString = [self timeForMessage: msgTime];
        result.timeLabel.stringValue = timeString;
554 555
    }

556 557 558 559 560 561 562
    bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
                                && sequence != FIRST_WITH_TIME) ? YES : NO;
    [result.photoView setHidden:!shouldDisplayAvatar];
    if (!isOutgoing && shouldDisplayAvatar) {
        auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
        [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
    }
563 564 565
    return result;
}

566
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
567
{
568 569 570 571 572 573 574
    double someWidth = tableView.frame.size.width * 0.7;

    auto* conv = [self getCurrentConversation];

    if (conv == nil)
        return 0;

575
    auto it = conv->interactions.begin();
576

577
    std::advance(it, row);
578

579 580 581
    auto interaction = it->second;

    MessageSequencing sequence = [self computeSequencingFor:row];
582

583 584 585 586 587 588 589
    bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;


    if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {

        if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
            NSString* name =  @(interaction.body.c_str());
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
590
            NSImage* image = [self getImageForFilePath:name];
591
            if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
592
                image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
593 594
            }
            if (image != nil) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
595 596 597 598 599 600 601 602 603 604
                CGFloat widthScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.width;
                CGFloat heightScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.height;
                CGFloat heigt = 0;
                if((widthScaleFactor >= 1) && (heightScaleFactor >= 1)) {
                    heigt = image.size.height;
                } else {
                    CGFloat scale = MIN(widthScaleFactor, heightScaleFactor);
                    heigt = image.size.height * scale;
                }
                return heigt + TIME_BOX_HEIGHT;
605 606 607 608
            }
        }
        return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
    }
609

610
    if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
611
        return GENERIC_CELL_HEIGHT;
612

613 614 615 616 617
    // TODO Implement interactions other than messages
    if(interaction.type != lrc::api::interaction::Type::TEXT) {
        return 0;
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
618 619 620 621
    NSString *text = @(interaction.body.c_str());
    text = [text removeEmptyLinesAtBorders];

    CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
622 623
    CGFloat singleLignMessageHeight = 15;

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
624 625
    bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;

626 627 628 629
    if (shouldDisplayTime) {
        return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
                   TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
630 631 632 633
    if(shouldApplyPadding) {
        return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
                   singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
    }
634 635 636
    return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
               singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
}
637

638
#pragma mark - message view parameters
639

640 641 642 643 644 645 646
-(NSString *) getDataTransferPath:(uint64_t)interactionId {
    lrc::api::datatransfer::Info info = {};
    convModel_->getTransferInfo(interactionId, info);
    double convertData = static_cast<double>(info.totalSize);
    return @(info.path.c_str());
}

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
647
-(NSImage*) getImageForFilePath: (NSString *) path {
648 649 650
    if (path.length <= 0) {return nil;}
    if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
    NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
651
    return transferedImage;
652
}
653

654 655 656 657 658
-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
    CGFloat horizaontalMargin = 6;
    NSMutableAttributedString* msgAttString =
    [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
                                           attributes:[self messageAttributes]];
659

660 661
    CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
    NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
662 663 664
    NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
    [[tv textStorage] setAttributedString:msgAttString];
    [tv sizeToFit];
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720
    return tv.frame.size;
}

-(MessageSequencing) computeSequencingFor:(NSInteger) row {
    auto* conv = [self getCurrentConversation];
    if (conv == nil)
    return SINGLE_WITHOUT_TIME;
    auto it = conv->interactions.begin();
    std::advance(it, row);
    auto interaction = it->second;
    if (interaction.type != lrc::api::interaction::Type::TEXT) {
        return SINGLE_WITH_TIME;
    }
    if (row == 0) {
        if (it == conv->interactions.end()) {
            return SINGLE_WITH_TIME;
        }
        auto nextIt = it;
        nextIt++;
        auto nextInteraction = nextIt->second;
        if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
            return SINGLE_WITH_TIME;
        }
        return FIRST_WITH_TIME;
    }

    if (row == conversationView.numberOfRows - 1) {
        if(it == conv->interactions.begin()) {
            return SINGLE_WITH_TIME;
        }
        auto previousIt = it;
        previousIt--;
        auto previousInteraction = previousIt->second;
        bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
        bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
        if (!timeChanged && !authorChanged) {
            return LAST_IN_SEQUENCE;
        }
        if (!timeChanged && authorChanged) {
            return SINGLE_WITHOUT_TIME;
        }
        return SINGLE_WITH_TIME;
    }
    if(it == conv->interactions.begin() || it == conv->interactions.end()) {
        return SINGLE_WITH_TIME;
    }
    auto previousIt = it;
    previousIt--;
    auto previousInteraction = previousIt->second;
    auto nextIt = it;
    nextIt++;
    auto nextInteraction = nextIt->second;

    bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
    bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
    bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
721 722 723 724 725 726 727
    if (previousInteraction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER ||
        previousInteraction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
        if(!sequenceWillChange) {
            return FIRST_WITH_TIME;
        }
        return SINGLE_WITH_TIME;
    }
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767
    if (!sequenceWillChange) {
        if (!timeChanged && !authorChanged) {
            return MIDDLE_IN_SEQUENCE;
        }
        if (timeChanged) {
            return FIRST_WITH_TIME;
        }
        return FIRST_WITHOUT_TIME;
    } if (!timeChanged && !authorChanged) {
        return LAST_IN_SEQUENCE;
    } if (timeChanged) {
        return SINGLE_WITH_TIME;
    }
    return SINGLE_WITHOUT_TIME;
}

-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
    return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
}

-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
    bool timeChanged = NO;
    NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
    NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
    bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
    bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
    if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
        timeChanged = YES;
    }
    return timeChanged;
}

-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
    bool authorChanged = YES;
    bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
    if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
        authorChanged = NO;
    }
    return authorChanged;
}
768

769 770 771
-(NSString *)timeForMessage:(NSDate*) msgTime {
    NSDate *today = [NSDate date];
    NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
772
    [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
    if ([[NSCalendar currentCalendar] compareDate:today
                                           toDate:msgTime
                                toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
        return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle];
    }

    if ([[NSCalendar currentCalendar] compareDate:today
                                           toDate:msgTime
                                toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
        [[NSCalendar currentCalendar] compareDate:today
                                           toDate:msgTime
                                toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
            [dateFormatter setDateFormat:@"MMM dd, HH:mm"];
            return [dateFormatter stringFromDate:msgTime];
        }

    [dateFormatter setDateFormat:@"HH:mm"];
    return [dateFormatter stringFromDate:msgTime];
791 792
}

793 794 795 796 797 798
#pragma mark - NSTableViewDataSource

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
    auto* conv = [self getCurrentConversation];

799 800 801 802
    if (conv)
        return conv->interactions.size();
    else
        return 0;
803 804
}

805 806
#pragma mark - Text formatting

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
807
- (NSMutableDictionary*) messageAttributes
808 809
{
    NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
810
    attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827
    attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
    return attrs;
}

- (NSParagraphStyle*) paragraphStyle
{
    /*
     The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
     NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
     to copy, we use the default one.

     The default values supplied by the default NSParagraphStyle are:
     Alignment   NSNaturalTextAlignment
     Tab stops   12 left-aligned tabs, spaced by 28.0 points
     Line break mode   NSLineBreakByWordWrapping
     All others   0.0
     */
828 829 830 831 832 833 834
    NSMutableParagraphStyle* aMutableParagraphStyle =
    [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
    [aMutableParagraphStyle setHeadIndent:1.0];
    [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
    return aMutableParagraphStyle;
}

835 836
#pragma mark - Actions

837
- (void)acceptIncomingFile:(id)sender {
838 839 840 841 842 843 844 845 846 847 848 849 850
    auto interId = [(IMTableCellView*)[[sender superview] superview] interaction];
    auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
    if (convModel_ && !convUid_.empty()) {
        NSSavePanel* filePicker = [NSSavePanel savePanel];
        [filePicker setNameFieldStringValue:@(inter.body.c_str())];

        if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
            const char* fullPath = [[filePicker URL] fileSystemRepresentation];
            convModel_->acceptTransfer(convUid_, interId, fullPath);
        }
    }
}

851
- (void)declineIncomingFile:(id)sender {
852 853 854 855 856 857
    auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
    if (convModel_ && !convUid_.empty()) {
        convModel_->cancelTransfer(convUid_, inter);
    }
}

858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894
- (void)imagePreview:(id)sender {
    uint64_t interId;
    if ([[sender superview] isKindOfClass:[IMTableCellView class]]) {
        interId = [(IMTableCellView*)[sender superview] interaction];
    } else if ([[[sender superview] superview] isKindOfClass:[IMTableCellView class]]) {
        interId = [(IMTableCellView*)[[sender superview] superview] interaction];
    } else {
        return;
    }
    auto it = [self getCurrentConversation]->interactions.find(interId);
    if (it == [self getCurrentConversation]->interactions.end()) {
        return;
    }
    auto& interaction = it->second;
    NSString* name =  @(interaction.body.c_str());
    if (([name rangeOfString:@"/"].location == NSNotFound)) {
        name = [self getDataTransferPath:interId];
    }
    previewImage = name;
    if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
        [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
    } else {
        [[QLPreviewPanel sharedPreviewPanel] updateController];
        [QLPreviewPanel sharedPreviewPanel].dataSource = self;
        [[QLPreviewPanel sharedPreviewPanel] setAnimationBehavior:NSWindowAnimationBehaviorDocumentWindow];
        [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
    }
}

- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
    return 1;
}

- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
    return [NSURL fileURLWithPath:previewImage];
}

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973
- (void) updateSendMessageHeight {
    NSAttributedString *msgAttString = messageField.attributedStringValue;
    NSRect frame = NSMakeRect(0, 0, messageField.frame.size.width, msgAttString.size.height);
    NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
    [[tv textStorage] setAttributedString:msgAttString];
    [tv sizeToFit];
    CGFloat height = tv.frame.size.height + MEESAGE_MARGIN * 2;
    CGFloat newHeight = MIN(SEND_PANEL_MAX_HEIGHT, MAX(SEND_PANEL_DEFAULT_HEIGHT, height));
    if(messagesBottomMargin.constant == newHeight) {
        return;
    }
    messagesBottomMargin.constant = newHeight;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        [self scrollToBottom];
        sendPanelHeight.constant = newHeight;
    });
}

- (IBAction)sendMessage:(id)sender {
    NSString* text = self.message;
    if (text && text.length > 0) {
        auto* conv = [self getCurrentConversation];
        convModel_->sendMessage(convUid_, std::string([text UTF8String]));
        self.message = @"";
        if(sendPanelHeight.constant != SEND_PANEL_DEFAULT_HEIGHT) {
            sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
            messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
            [self scrollToBottom];
        }
    }
}

- (IBAction)openEmojy:(id)sender {
    [messageField.window makeFirstResponder: messageField];
    [[messageField currentEditor] moveToEndOfLine:nil];
    [NSApp orderFrontCharacterPalette: messageField];
}

- (IBAction)sendFile:(id)sender {
    NSOpenPanel* filePicker = [NSOpenPanel openPanel];
    [filePicker setCanChooseFiles:YES];
    [filePicker setCanChooseDirectories:NO];
    [filePicker setAllowsMultipleSelection:NO];

    if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
        if ([[filePicker URLs] count] == 1) {
            NSURL* url = [[filePicker URLs] objectAtIndex:0];
            const char* fullPath = [url fileSystemRepresentation];
            NSString* fileName = [url lastPathComponent];
            if (convModel_) {
                auto* conv = [self getCurrentConversation];
                convModel_->sendFile(convUid_, std::string(fullPath), std::string([fileName UTF8String]));
            }
        }
    }
}


#pragma mark - NSTextFieldDelegate

- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
{
    if (commandSelector == @selector(insertNewline:)) {
        if(self.message.length > 0) {
            [self sendMessage: nil];
        } else if(messagesBottomMargin.constant != SEND_PANEL_DEFAULT_HEIGHT) {
            sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
            messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
            [self scrollToBottom];
        }
        return YES;
    }
    return NO;
}

- (void)controlTextDidChange:(NSNotification *)aNotification {
    [self updateSendMessageHeight];
}

974
@end