MessagesVC.mm 41.2 KB
Newer Older
Andreas Traczyk's avatar
Andreas Traczyk committed
1

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
2
/*
Sébastien Blin's avatar
Sébastien Blin committed
3
 *  Copyright (C) 2015-2019 Savoir-faire Linux Inc.
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
4
 *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
5
 *          Anthony Léonard <anthony.leonard@savoirfairelinux.com>
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
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
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
26
#import <globalinstances.h>
27
#import <api/interaction.h>
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
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

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
40

41
@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource> {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
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;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
49

50
    std::string convUid_;
51
    lrc::api::ConversationModel* convModel_;
52
    const lrc::api::conversation::Info* cachedConv_;
53
    QMetaObject::Connection newInteractionSignal_;
54 55 56 57 58

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


@end

67 68
// Tags for view
NSInteger const GENERIC_INT_TEXT_TAG = 100;
69 70 71 72 73 74 75 76
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
77 78 79
NSInteger const MEESAGE_MARGIN = 21;
NSInteger const SEND_PANEL_DEFAULT_HEIGHT = 60;
NSInteger const SEND_PANEL_MAX_HEIGHT = 120;
80

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
81 82
@implementation MessagesVC

83

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
//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
102 103 104
    [[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
    [messageField setFocusRingType:NSFocusRingTypeNone];
    [conversationView setWantsLayer:YES];
105
}
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
106 107 108 109 110 111 112 113 114 115 116 117 118 119

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

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

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

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

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
134 135 136 137 138 139 140 141 142 143 144
-(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];
    }
}

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

    if (cachedConv_ != nil)
        return cachedConv_;

153
    auto it = getConversationFromUid(convUid_, *convModel_);
154
    if (it != convModel_->allFilteredConversations().end())
155
        cachedConv_ = &(*it);
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
156

157 158 159
    return cachedConv_;
}

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

    if (conv == nil)
        return;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
165 166 167 168 169 170
    auto it = conv->interactions.find(uid);
    if (it == conv->interactions.end()) {
        return;
    }
    auto itIndex = distance(conv->interactions.begin(),it);
    NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:itIndex];
171
    //reload previous message to update bubbleview
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
172 173 174 175 176 177 178 179
    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];
        }
180 181 182 183 184 185
    }
    if (update) {
        [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
    }
    [conversationView reloadDataForRowIndexes: indexSet
                                columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
186 187 188 189 190 191
    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)) {
192 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
        [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];
    }
}

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

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

227 228 229 230
    // Signal triggered when messages are received or their status updated
    QObject::disconnect(newInteractionSignal_);
    QObject::disconnect(interactionStatusUpdatedSignal_);
    newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
231 232 233 234 235 236 237
                                             [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];
                                             });
238 239 240 241 242
    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;
243 244 245 246 247
                                                           bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
                                                           if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
                                                               convModel_->refreshFilter();
                                                           }
                                                           [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
248
                                                       });
249 250 251 252 253 254 255 256 257 258 259 260 261

    // 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
262 263 264 265 266 267 268 269 270 271 272 273
    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;
274
    [conversationView reloadData];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
275
    [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
276 277 278 279 280 281 282 283 284 285 286 287
    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)];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
288 289
}

290
#pragma mark - configure cells
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
291

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

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

    return result;
}

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

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

    NSString* fileName = @"incoming file";

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

    // Then status label is updated if needed
    switch (status) {
371
            [result.statusLabel setTextColor:[NSColor textColor]];
372
        case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
373
            [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard's avatar
Anthony Léonard committed
374
            [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
375 376
            break;
        case lrc::api::interaction::Status::TRANSFER_CANCELED:
377
            [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard's avatar
Anthony Léonard committed
378
            [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
379 380
            break;
        case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
381
            [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard's avatar
Anthony Léonard committed
382
            [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
383 384
            break;
        case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
385
             [result.statusLabel setTextColor:[NSColor textColor]];
386 387
            [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
            break;
388
    }
389
    result.transferedImage.image = nil;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
390
    [result.openImagebutton setHidden:YES];
391 392 393 394
    [result.msgBackground setHidden:NO];
    [result invalidateImageConstraints];
    NSString* name =  @(interaction.body.c_str());
    if (name.length > 0) {
395
       fileName = [name lastPathComponent];
396
    }
397 398 399 400 401 402 403 404 405 406
    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;
407
    if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
408 409 410 411 412 413
        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
414
        NSImage* image = [self getImageForFilePath:name];
415
        if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
416
            image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
417 418
        }
        if(image != nil) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
419 420 421 422
            result.transferedImage.image = image;
            [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
            [result.openImagebutton setAction:@selector(imagePreview:)];
            [result.openImagebutton setTarget:self];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
423
            [result.openImagebutton setHidden:NO];
424 425 426 427 428 429 430 431 432 433 434 435
        }
    }
    [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)))];
    }
436 437 438
    return result;
}

439 440
#pragma mark - NSTableViewDelegate methods
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
441 442 443 444
{
    return YES;
}

445
- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
446
{
447
    return NO;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
448 449
}

450
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
451
{
452
    
453 454 455 456 457
    auto* conv = [self getCurrentConversation];

    if (conv == nil)
        return nil;

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

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

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
462
    IMTableCellView* result;
463
    auto interaction = it->second;
464 465
    bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);

466 467 468 469 470 471 472 473 474 475
    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:
476
            return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
477
            break;
478
        case lrc::api::interaction::Type::CONTACT:
479 480 481 482 483
        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];
        }
484 485
        default:  // If interaction is not of a known type
            return nil;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
486
    }
487 488 489 490 491 492 493 494 495 496 497 498
    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
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
    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];
514
    bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
515
    bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
516 517 518
    [result.msgBackground setNeedsDisplay:YES];
    [result setNeedsDisplay:YES];
    [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
519

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

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

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

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

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
    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];

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

555 556 557 558 559 560 561
    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)))];
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
562 563 564
    return result;
}

565
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
566
{
567 568 569 570 571 572 573
    double someWidth = tableView.frame.size.width * 0.7;

    auto* conv = [self getCurrentConversation];

    if (conv == nil)
        return 0;

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

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

578 579 580
    auto interaction = it->second;

    MessageSequencing sequence = [self computeSequencingFor:row];
581

582 583 584 585 586 587 588
    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
589
            NSImage* image = [self getImageForFilePath:name];
590
            if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
591
                image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
592 593
            }
            if (image != nil) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
594 595 596 597 598 599 600 601 602 603
                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;
604 605 606 607
            }
        }
        return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
    }
608

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

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

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

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

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

625 626 627 628
    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
629 630 631 632
    if(shouldApplyPadding) {
        return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
                   singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
    }
633 634 635
    return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
               singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
}
636

637
#pragma mark - message view parameters
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
638

639 640 641 642 643 644 645
-(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
646
-(NSImage*) getImageForFilePath: (NSString *) path {
647 648 649
    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
650
    return transferedImage;
651
}
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
652

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

659 660
    CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
    NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
661 662 663
    NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
    [[tv textStorage] setAttributedString:msgAttString];
    [tv sizeToFit];
664 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
    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
720 721 722 723 724 725 726
    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;
    }
727 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
    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;
}
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
767

768 769 770
-(NSString *)timeForMessage:(NSDate*) msgTime {
    NSDate *today = [NSDate date];
    NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
771
    [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
    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];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
790 791
}

792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809
- (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;
    });
}

810 811 812 813 814 815
#pragma mark - NSTableViewDataSource

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

816 817 818 819
    if (conv)
        return conv->interactions.size();
    else
        return 0;
820 821
}

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
822 823
#pragma mark - Text formatting

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
824
- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
825 826
{
    NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
827
    attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844
    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
     */
845 846 847 848 849 850 851
    NSMutableParagraphStyle* aMutableParagraphStyle =
    [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
    [aMutableParagraphStyle setHeadIndent:1.0];
    [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
    return aMutableParagraphStyle;
}

852 853
#pragma mark - Actions

854
- (void)acceptIncomingFile:(id)sender {
855 856 857 858 859 860 861 862 863 864 865 866 867
    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);
        }
    }
}

868
- (void)declineIncomingFile:(id)sender {
869 870 871 872 873 874
    auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
    if (convModel_ && !convUid_.empty()) {
        convModel_->cancelTransfer(convUid_, inter);
    }
}

875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896
- (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 {
897 898 899
        dispatch_async(dispatch_get_main_queue(), ^{
            [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:self];
        });
900 901 902
    }
}

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
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
- (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];
}

964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987
#pragma mark - QLPreviewPanelDataSource

-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
    panel.dataSource = self;
}

- (void)endPreviewPanelControl:(QLPreviewPanel *)panel {
    panel.dataSource = nil;
}

-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
{
    return YES;
}

- (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
988
@end