SmartViewVC.mm 32.7 KB
Newer Older
1
/*
Sébastien Blin's avatar
Sébastien Blin committed
2
 *  Copyright (C) 2015-2019 Savoir-faire Linux Inc.
3
 *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
4 5
 *  Author: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
 *  Author: 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
 *
 *  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 "SmartViewVC.h"

Andreas Traczyk's avatar
Andreas Traczyk committed
24 25 26
//std
#import <sstream>

27 28 29 30 31 32 33 34
//Qt
#import <QtMacExtras/qmacfunctions.h>
#import <QPixmap>
#import <QIdentityProxyModel>
#import <QItemSelectionModel>

//LRC
#import <globalinstances.h>
35 36 37 38 39 40
#import <api/newaccountmodel.h>
#import <api/conversationmodel.h>
#import <api/account.h>
#import <api/contact.h>
#import <api/contactmodel.h>
#import <api/newcallmodel.h>
41 42

#import "delegates/ImageManipulationDelegate.h"
Alexandre Lision's avatar
Alexandre Lision committed
43
#import "views/HoverTableRowView.h"
44
#import "views/IconButton.h"
45
#import "views/RingTableView.h"
Alexandre Lision's avatar
Alexandre Lision committed
46
#import "views/ContextualTableCellView.h"
47
#import "utils.h"
48
#import "RingWindowController.h"
49

50
@interface SmartViewVC () <NSTableViewDelegate, NSTableViewDataSource, NSPopoverDelegate, ContextMenuDelegate, KeyboardShortcutDelegate> {
51

52
    NSPopover* addToContactPopover;
53 54

    //UI elements
55
    __unsafe_unretained IBOutlet RingTableView* smartView;
56
    __unsafe_unretained IBOutlet NSSearchField* searchField;
57
    __strong IBOutlet NSSegmentedControl *listTypeSelector;
Andreas Traczyk's avatar
Andreas Traczyk committed
58
    __strong IBOutlet NSLayoutConstraint *listTypeSelectorHeight;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
59
    __strong IBOutlet NSLayoutConstraint *listTypeSelectorBottom;
60
    bool selectorIsPresent;
61

Andreas Traczyk's avatar
Andreas Traczyk committed
62
    QMetaObject::Connection modelSortedConnection_, modelUpdatedConnection_, filterChangedConnection_, newConversationConnection_, conversationRemovedConnection_, newInteractionConnection_, interactionStatusUpdatedConnection_, conversationClearedConnection;
63

Andreas Traczyk's avatar
Andreas Traczyk committed
64
    lrc::api::ConversationModel* convModel_;
65
    std::string selectedUid_;
66
    lrc::api::profile::Type currentFilterType;
67 68

    __unsafe_unretained IBOutlet RingWindowController *delegate;
69 70 71 72 73 74
}

@end

@implementation SmartViewVC

75 76
@synthesize tabbar;

77
// Tags for views
78 79
NSInteger const IMAGE_TAG           = 100;
NSInteger const DISPLAYNAME_TAG     = 200;
Andreas Traczyk's avatar
Andreas Traczyk committed
80 81 82 83 84 85 86 87 88 89
NSInteger const NOTIFICATONS_TAG    = 300;
NSInteger const RING_ID_LABEL       = 400;
NSInteger const PRESENCE_TAG        = 500;
NSInteger const TOTALMSGS_TAG       = 600;
NSInteger const TOTALINVITES_TAG    = 700;
NSInteger const DATE_TAG            = 800;
NSInteger const SNIPPET_TAG         = 900;
NSInteger const ADD_BUTTON_TAG            = 1000;
NSInteger const REFUSE_BUTTON_TAG         = 1100;
NSInteger const BLOCK_BUTTON_TAG          = 1200;
90

91 92 93 94
// Segment indices for smartlist selector
NSInteger const CONVERSATION_SEG    = 0;
NSInteger const REQUEST_SEG         = 1;

95 96 97
- (void)awakeFromNib
{
    NSLog(@"INIT SmartView VC");
98 99
    //get selected account
    //encapsulate conversationmodel in local version
100 101 102 103

    [smartView setTarget:self];
    [smartView setDoubleAction:@selector(placeCall:)];

104 105 106
    [smartView setContextMenuDelegate:self];
    [smartView setShortcutsDelegate:self];

107
    [smartView setDataSource: self];
108
    currentFilterType = lrc::api::profile::Type::RING;
109
    selectorIsPresent = true;
Andreas Traczyk's avatar
Andreas Traczyk committed
110 111

    smartView.selectionHighlightStyle = NSTableViewSelectionHighlightStyleNone;
112

113 114
}

115
- (void)placeCall:(id)sender
116
{
117 118 119 120 121 122
    NSInteger row;
    if (sender != nil && [sender clickedRow] != -1)
        row = [sender clickedRow];
    else if ([smartView selectedRow] != -1)
        row = [smartView selectedRow];
    else
123
        return;
Andreas Traczyk's avatar
Andreas Traczyk committed
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
    if (convModel_ == nil)
        return;

    auto conv = convModel_->filteredConversation(row);
    convModel_->placeCall(conv.uid);
}

-(void) reloadSelectorNotifications
{
    NSTextField* totalMsgsCount = [self.view viewWithTag:TOTALMSGS_TAG];
    NSTextField* totalInvites = [self.view viewWithTag:TOTALINVITES_TAG];

    if (!selectorIsPresent) {
        [totalMsgsCount setHidden:true];
        [totalInvites setHidden:true];
139
        return;
Andreas Traczyk's avatar
Andreas Traczyk committed
140
    }
141

Andreas Traczyk's avatar
Andreas Traczyk committed
142 143 144 145 146 147 148 149 150 151 152 153
    auto ringConversations = convModel_->getFilteredConversations(lrc::api::profile::Type::RING);
    int totalUnreadMessages = 0;
    std::for_each(ringConversations.begin(), ringConversations.end(),
        [&totalUnreadMessages, self] (const auto& conversation) {
            totalUnreadMessages += convModel_->getNumberOfUnreadMessagesFor(conversation.uid);
        });
    [totalMsgsCount setHidden:(totalUnreadMessages == 0)];
    [totalMsgsCount setIntValue:totalUnreadMessages];

    auto totalRequests = [self chosenAccount].contactModel->pendingRequestCount();
    [totalInvites setHidden:(totalRequests == 0)];
    [totalInvites setIntValue:totalRequests];
154 155
}

156
-(void) reloadData
157
{
158
    [smartView deselectAll:nil];
Andreas Traczyk's avatar
Andreas Traczyk committed
159
    if (convModel_ == nil)
160
        return;
161

Andreas Traczyk's avatar
Andreas Traczyk committed
162 163 164
    [self reloadSelectorNotifications];

    if (!convModel_->owner.contactModel->hasPendingRequests()) {
165 166 167
        if (currentFilterType == lrc::api::profile::Type::PENDING) {
            [self selectConversationList];
        }
168
        if (selectorIsPresent) {
Andreas Traczyk's avatar
Andreas Traczyk committed
169
            listTypeSelectorHeight.constant = 0.0;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
170
            listTypeSelectorBottom.priority = 250;
Andreas Traczyk's avatar
Andreas Traczyk committed
171
            [listTypeSelector setHidden:YES];
172 173
            selectorIsPresent = false;
        }
174
    } else {
175 176
        if (!selectorIsPresent) {
            [listTypeSelector setSelected:YES forSegment:CONVERSATION_SEG];
Andreas Traczyk's avatar
Andreas Traczyk committed
177
            listTypeSelectorHeight.constant = 18.0;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
178
            listTypeSelectorBottom.priority = 999;
Andreas Traczyk's avatar
Andreas Traczyk committed
179
            [listTypeSelector setHidden:NO];
180 181
            selectorIsPresent = true;
        }
182 183
    }

184
    [smartView reloadData];
185
    [smartView layoutSubtreeIfNeeded];
186

Andreas Traczyk's avatar
Andreas Traczyk committed
187 188 189 190
    if (!selectedUid_.empty() && convModel_ != nil) {
        auto it = getConversationFromUid(selectedUid_, *convModel_);
        if (it != convModel_->allFilteredConversations().end()) {
            NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - convModel_->allFilteredConversations().begin())];
191
            [smartView selectRowIndexes:indexSet byExtendingSelection:NO];
192 193 194
        }
    }

195 196 197
    [smartView scrollToBeginningOfDocument:nil];
}

198 199
-(void) reloadConversationWithUid:(NSString *)uid
{
Andreas Traczyk's avatar
Andreas Traczyk committed
200 201 202 203 204 205 206 207 208
    if (convModel_ == nil) {
        return;
    }

    auto it = getConversationFromUid(std::string([uid UTF8String]), *convModel_);
    if (it != convModel_->allFilteredConversations().end()) {
        NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - convModel_->allFilteredConversations().begin())];
        [smartView reloadDataForRowIndexes:indexSet
                             columnIndexes:[NSIndexSet indexSetWithIndex:0]];
209 210
    }
}
211

212
- (BOOL)setConversationModel:(lrc::api::ConversationModel *)conversationModel
213
{
Andreas Traczyk's avatar
Andreas Traczyk committed
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
    if (convModel_ == conversationModel) {
        return false;
    }

    convModel_ = conversationModel;
    selectedUid_.clear(); // Clear selected conversation as the selected account is being changed
    QObject::disconnect(modelSortedConnection_);
    QObject::disconnect(modelUpdatedConnection_);
    QObject::disconnect(filterChangedConnection_);
    QObject::disconnect(newConversationConnection_);
    QObject::disconnect(conversationRemovedConnection_);
    QObject::disconnect(conversationClearedConnection);
    QObject::disconnect(interactionStatusUpdatedConnection_);
    QObject::disconnect(newInteractionConnection_);
    [self reloadData];
    if (convModel_ != nil) {
        modelSortedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
231 232 233
                                                        [self] (){
                                                            [self reloadData];
                                                        });
Andreas Traczyk's avatar
Andreas Traczyk committed
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
        modelUpdatedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::conversationUpdated,
                                                        [self] (const std::string& convUid){
                                                            [self reloadConversationWithUid: [NSString stringWithUTF8String:convUid.c_str()]];
                                                        });
        filterChangedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
                                                        [self] (){
                                                            [self reloadData];
                                                        });
        newConversationConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newConversation,
                                                        [self] (const std::string& convUid) {
                                                            [self reloadData];
                                                            [self updateConversationForNewContact:[NSString stringWithUTF8String:convUid.c_str()]];
                                                        });
        conversationRemovedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::conversationRemoved,
                                                        [self] (){
                                                            [delegate listTypeChanged];
                                                            [self reloadData];
                                                        });
        conversationClearedConnection = QObject::connect(convModel_, &lrc::api::ConversationModel::conversationCleared,
                                                        [self] (const std::string& convUid){
                                                            [self deselect];
                                                            [delegate listTypeChanged];
                                                        });
        interactionStatusUpdatedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
                                                        [self] (const std::string& convUid) {
                                                            if (convUid != selectedUid_)
                                                                return;
                                                            [self reloadConversationWithUid: [NSString stringWithUTF8String:convUid.c_str()]];
                                                        });
        newInteractionConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
                                                        [self](const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
                                                            if (convUid == selectedUid_) {
                                                                convModel_->clearUnreadInteractions(convUid);
                                                            }
                                                        });
        convModel_->setFilter(""); // Reset the filter
270
    }
Andreas Traczyk's avatar
Andreas Traczyk committed
271 272
    [searchField setStringValue:@""];
    return true;
273 274 275 276 277 278 279 280 281 282
}

-(void)selectConversation:(const lrc::api::conversation::Info&)conv model:(lrc::api::ConversationModel*)model;
{
    auto& uid = conv.uid;
    if (selectedUid_ == uid)
        return;

    [self setConversationModel:model];

Andreas Traczyk's avatar
Andreas Traczyk committed
283 284 285 286 287 288 289 290 291
    if (convModel_ == nil) {
        return;
    }

    auto it = getConversationFromUid(selectedUid_, *convModel_);
    if (it != convModel_->allFilteredConversations().end()) {
        NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - convModel_->allFilteredConversations().begin())];
        [smartView selectRowIndexes:indexSet byExtendingSelection:NO];
        selectedUid_ = uid;
292 293 294
    }
}

295 296 297 298 299 300
-(void)deselect
{
    selectedUid_.clear();
    [smartView deselectAll:nil];
}

301
-(void) clearConversationModel {
Andreas Traczyk's avatar
Andreas Traczyk committed
302
    convModel_ = nil;
303 304
    [self deselect];
    [smartView reloadData];
305 306 307 308
    if (selectorIsPresent) {
        [listTypeSelector removeFromSuperview];
        selectorIsPresent = false;
    }
309 310
}

Anthony Léonard's avatar
Anthony Léonard committed
311 312
- (IBAction) listTypeChanged:(id)sender
{
Andreas Traczyk's avatar
Andreas Traczyk committed
313
    selectedUid_.clear();
Anthony Léonard's avatar
Anthony Léonard committed
314
    NSInteger selectedItem = [sender selectedSegment];
315 316
    switch (selectedItem) {
        case CONVERSATION_SEG:
317
            if (currentFilterType != lrc::api::profile::Type::RING) {
Andreas Traczyk's avatar
Andreas Traczyk committed
318
                convModel_->setFilter(lrc::api::profile::Type::RING);
319 320 321
                [delegate listTypeChanged];
                currentFilterType = lrc::api::profile::Type::RING;
            }
322 323
            break;
        case REQUEST_SEG:
324
            if (currentFilterType != lrc::api::profile::Type::PENDING) {
Andreas Traczyk's avatar
Andreas Traczyk committed
325
                convModel_->setFilter(lrc::api::profile::Type::PENDING);
326 327 328
                [delegate listTypeChanged];
                currentFilterType = lrc::api::profile::Type::PENDING;
            }
329 330 331
            break;
        default:
            NSLog(@"Invalid item selected in list selector: %d", selectedItem);
Anthony Léonard's avatar
Anthony Léonard committed
332 333 334 335 336
    }
}

-(void) selectConversationList
{
337 338
    if (currentFilterType == lrc::api::profile::Type::RING)
        return;
339
    [listTypeSelector setSelectedSegment:CONVERSATION_SEG];
340 341 342 343

    // Do not invert order of the next two lines or stack overflow
    // may happen on -(void) reloadData call if filter is currently set to PENDING
    currentFilterType = lrc::api::profile::Type::RING;
Andreas Traczyk's avatar
Andreas Traczyk committed
344 345
    convModel_->setFilter(lrc::api::profile::Type::RING);
    convModel_->setFilter("");
Anthony Léonard's avatar
Anthony Léonard committed
346 347
}

348 349 350 351 352 353 354
-(void) selectPendingList
{
    if (currentFilterType == lrc::api::profile::Type::PENDING)
        return;
    [listTypeSelector setSelectedSegment:REQUEST_SEG];

    currentFilterType = lrc::api::profile::Type::PENDING;
Andreas Traczyk's avatar
Andreas Traczyk committed
355 356
    convModel_->setFilter(lrc::api::profile::Type::PENDING);
    convModel_->setFilter("");
357 358
}

359
#pragma mark - NSTableViewDelegate methods
360

361
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
362 363 364 365
{
    return YES;
}

366
- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
367 368 369 370
{
    return NO;
}

371
- (void)tableViewSelectionDidChange:(NSNotification *)notification
372
{
373
    NSInteger row = [notification.object selectedRow];
374
    NSInteger rows = [smartView numberOfRows];
375

376
    for (int i = 0; i< rows; i++) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
377 378
        HoverTableRowView* cellRowView = [smartView rowViewAtRow:i makeIfNecessary: NO];
        [cellRowView drawSelection: (i == row)];
379
    }
Andreas Traczyk's avatar
Andreas Traczyk committed
380

381
    if (row == -1)
382
        return;
Andreas Traczyk's avatar
Andreas Traczyk committed
383
    if (convModel_ == nil)
384
        return;
385

Andreas Traczyk's avatar
Andreas Traczyk committed
386
    auto uid = convModel_->filteredConversation(row).uid;
387 388
    if (selectedUid_ != uid) {
        selectedUid_ = uid;
Andreas Traczyk's avatar
Andreas Traczyk committed
389 390 391
        convModel_->selectConversation(uid);
        convModel_->clearUnreadInteractions(uid);
        [self reloadSelectorNotifications];
392 393 394
    }
}

395 396 397 398 399 400
- (NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row
{
    return [tableView makeViewWithIdentifier:@"HoverRowView" owner:nil];
}

- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
401
{
Andreas Traczyk's avatar
Andreas Traczyk committed
402
    if (convModel_ == nil)
403
        return nil;
Alexandre Lision's avatar
Alexandre Lision committed
404

Andreas Traczyk's avatar
Andreas Traczyk committed
405
    auto conversation = convModel_->filteredConversation(row);
Alexandre Lision's avatar
Alexandre Lision committed
406
    NSTableCellView* result;
407

408
    result = [tableView makeViewWithIdentifier:@"MainCell" owner:tableView];
409

Andreas Traczyk's avatar
Andreas Traczyk committed
410
    NSTextField* unreadCount = [result viewWithTag:NOTIFICATONS_TAG];
411 412
    [unreadCount setHidden:(conversation.unreadMessages == 0)];
    [unreadCount setIntValue:conversation.unreadMessages];
413
    NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG];
Andreas Traczyk's avatar
Andreas Traczyk committed
414
    NSTextField* displayRingID = [result viewWithTag:RING_ID_LABEL];
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
415 416 417 418 419 420 421
    NSTextField* lastInteractionDate = [result viewWithTag:DATE_TAG];
    NSTextField* interactionSnippet = [result viewWithTag:SNIPPET_TAG];
    [displayName setStringValue:@""];
    [displayRingID setStringValue:@""];
    [lastInteractionDate setStringValue:@""];
    [interactionSnippet setStringValue:@""];
    NSImageView* photoView = [result viewWithTag:IMAGE_TAG];
Andreas Traczyk's avatar
Andreas Traczyk committed
422 423
    NSString* displayNameString = bestNameForConversation(conversation, *convModel_);
    NSString* displayIDString = bestIDForConversation(conversation, *convModel_);
424 425
    if(displayNameString.length == 0 || [displayNameString isEqualToString:displayIDString]) {
        [displayName setStringValue:displayIDString];
426
        [displayRingID setHidden:YES];
427 428 429 430
    }
    else {
        [displayName setStringValue:displayNameString];
        [displayRingID setStringValue:displayIDString];
431
        [displayRingID setHidden:NO];
432
    }
433
    auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
    NSImage* image = QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(conversation, convModel_->owner)));
    if(image) {
        [NSLayoutConstraint deactivateConstraints:[photoView constraints]];
        NSArray* constraints = [NSLayoutConstraint
                                constraintsWithVisualFormat:@"H:[photoView(54)]"
                                options:0
                                metrics:nil                                                                          views:NSDictionaryOfVariableBindings(photoView)];
        [NSLayoutConstraint activateConstraints:constraints];
    } else {
        [NSLayoutConstraint deactivateConstraints:[photoView constraints]];
        NSArray* constraints = [NSLayoutConstraint
                                constraintsWithVisualFormat:@"H:[photoView(0)]"
                                options:0
                                metrics:nil                                                                          views:NSDictionaryOfVariableBindings(photoView)];
        [NSLayoutConstraint activateConstraints:constraints];
    }
    [photoView setImage: image];
451 452

    NSView* presenceView = [result viewWithTag:PRESENCE_TAG];
453 454 455 456 457 458 459 460 461 462
    [presenceView setHidden:YES];
    if (!conversation.participants.empty()){
        try {
            auto contact = convModel_->owner.contactModel->getContact(conversation.participants[0]);
            if (contact.isPresent) {
                [presenceView setHidden:NO];
            }
        } catch (std::out_of_range& e) {
            NSLog(@"viewForTableColumn: getContact - out of range");
        }
463
    }
464

Andreas Traczyk's avatar
Andreas Traczyk committed
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
    NSButton* addContactButton = [result viewWithTag:ADD_BUTTON_TAG];
    NSButton* refuseContactButton = [result viewWithTag:REFUSE_BUTTON_TAG];
    NSButton* blockContactButton = [result viewWithTag:BLOCK_BUTTON_TAG];
    [addContactButton setHidden:YES];
    [refuseContactButton setHidden:YES];
    [blockContactButton setHidden:YES];

    if (profileType(conversation, *convModel_) == lrc::api::profile::Type::PENDING) {
        [lastInteractionDate setHidden:true];
        [interactionSnippet setHidden:true];
        [addContactButton setHidden:NO];
        [refuseContactButton setHidden:NO];
        [blockContactButton setHidden:NO];
        [addContactButton setAction:@selector(acceptInvitation:)];
        [addContactButton setTarget:self];
        [refuseContactButton setAction:@selector(refuseInvitation:)];
        [refuseContactButton setTarget:self];
        [blockContactButton setAction:@selector(blockPendingContact:)];
        [blockContactButton setTarget:self];
        return result;
    }

    [lastInteractionDate setHidden:false];

    [interactionSnippet setHidden:false];

    auto lastUid = conversation.lastMessageUid;
    if (conversation.interactions.find(lastUid) != conversation.interactions.end()) {
        // last interaction snippet
        std::string lastInteractionSnippet = conversation.interactions[lastUid].body;
        std::stringstream ss(lastInteractionSnippet);
        std::getline(ss, lastInteractionSnippet);
        NSString* lastInteractionSnippetFixedString = [[NSString stringWithUTF8String:lastInteractionSnippet.c_str()]
                                                       stringByReplacingOccurrencesOfString:@"🕽" withString:@""];
        lastInteractionSnippetFixedString = [lastInteractionSnippetFixedString stringByReplacingOccurrencesOfString:@"📞" withString:@""];
        if (conversation.interactions[lastUid].type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER
            || conversation.interactions[lastUid].type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
502
            lastInteractionSnippetFixedString = [lastInteractionSnippetFixedString lastPathComponent];
Andreas Traczyk's avatar
Andreas Traczyk committed
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
        }
        [interactionSnippet setStringValue:lastInteractionSnippetFixedString];

        // last interaction date/time
        std::time_t lastInteractionTimestamp = conversation.interactions[lastUid].timestamp;
        std::time_t now = std::time(nullptr);
        char interactionDay[64];
        char nowDay[64];
        std::strftime(interactionDay, sizeof(interactionDay), "%D", std::localtime(&lastInteractionTimestamp));
        std::strftime(nowDay, sizeof(nowDay), "%D", std::localtime(&now));
        if (std::string(interactionDay) == std::string(nowDay)) {
            char interactionTime[64];
            std::strftime(interactionTime, sizeof(interactionTime), "%R", std::localtime(&lastInteractionTimestamp));
            [lastInteractionDate setStringValue:[NSString stringWithUTF8String:interactionTime]];
        } else {
            [lastInteractionDate setStringValue:[NSString stringWithUTF8String:interactionDay]];
        }
    }

522 523 524
    return result;
}

525
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
526
{
527 528 529 530 531 532 533
    return 60.0;
}

#pragma mark - NSTableDataSource methods

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
Andreas Traczyk's avatar
Andreas Traczyk committed
534 535
    if (tableView == smartView && convModel_ != nullptr) {
        return convModel_->allFilteredConversations().size();
536 537 538
    }

    return 0;
539 540
}

541
- (void)startCallForRow:(id)sender {
542 543 544 545 546 547 548
    NSInteger row = [smartView rowForView:sender];
    [smartView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
    [self placeCall:nil];
}

- (IBAction)hangUpClickedAtRow:(id)sender {
    NSInteger row = [smartView rowForView:sender];
549

550 551
    if (row == -1)
        return;
Andreas Traczyk's avatar
Andreas Traczyk committed
552
    if (convModel_ == nil)
553
        return;
554

Andreas Traczyk's avatar
Andreas Traczyk committed
555
    auto conv = convModel_->filteredConversation(row);
556
    auto& callId = conv.callId;
557

558
    if (callId.empty())
559 560
        return;

Andreas Traczyk's avatar
Andreas Traczyk committed
561
    auto* callModel = convModel_->owner.callModel.get();
562
    callModel->hangUp(callId);
Alexandre Lision's avatar
Alexandre Lision committed
563
}
564

565 566 567 568 569 570 571 572 573 574 575
- (void) displayErrorModalWithTitle:(NSString*) title WithMessage:(NSString*) message
{
    NSAlert* alert = [NSAlert alertWithMessageText:title
                                     defaultButton:@"Ok"
                                   alternateButton:nil
                                       otherButton:nil
                         informativeTextWithFormat:message];

    [alert beginSheetModalForWindow:self.view.window modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
}

576
- (void) processSearchFieldInput
577
{
Andreas Traczyk's avatar
Andreas Traczyk committed
578 579 580 581 582
    if (convModel_ == nil) {
        return;
    }

    convModel_->setFilter(std::string([[searchField stringValue] UTF8String]));
583 584
}

585
-(const lrc::api::account::Info&) chosenAccount
586
{
Andreas Traczyk's avatar
Andreas Traczyk committed
587
    return convModel_->owner;
588 589
}

590 591 592
- (void) clearSearchField
{
    [searchField setStringValue:@""];
593
    [self processSearchFieldInput];
594 595
}

596
-(void)updateConversationForNewContact:(NSString *)uId {
Andreas Traczyk's avatar
Andreas Traczyk committed
597
    if (convModel_ == nil) {
598 599 600 601
        return;
    }
    [self clearSearchField];
    auto uid = std::string([uId UTF8String]);
Andreas Traczyk's avatar
Andreas Traczyk committed
602 603
    auto it = getConversationFromUid(uid, *convModel_);
    if (it != convModel_->allFilteredConversations().end()) {
604
        @try {
Andreas Traczyk's avatar
Andreas Traczyk committed
605
            auto contact = convModel_->owner.contactModel->getContact(it->participants[0]);
606 607
            if (!contact.profileInfo.uri.empty() && contact.profileInfo.uri.compare(selectedUid_) == 0) {
                selectedUid_ = uid;
Andreas Traczyk's avatar
Andreas Traczyk committed
608
                convModel_->selectConversation(uid);
609 610 611 612 613 614 615
            }
        } @catch (NSException *exception) {
            return;
        }
    }
}

616 617 618 619 620 621
/**
 Copy a NSString in the general Pasteboard

 @param sender the NSObject containing the represented object to copy
 */
- (void) copyStringToPasteboard:(id) sender
622 623 624 625 626 627
{
    NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
    [pasteBoard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
    [pasteBoard setString:[sender representedObject] forType:NSStringPboardType];
}

Alexandre Lision's avatar
Alexandre Lision committed
628
#pragma NSTextFieldDelegate
629 630 631

- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
{
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
632 633 634
    if (commandSelector != @selector(insertNewline:) || [[searchField stringValue] isEqual:@""]) {
        return NO;
    }
Andreas Traczyk's avatar
Andreas Traczyk committed
635
    if (convModel_ == nil) {
636 637 638
        [self displayErrorModalWithTitle:NSLocalizedString(@"No account available", @"Displayed as RingID when no accounts are available for selection") WithMessage:NSLocalizedString(@"Navigate to preferences to create a new account", @"Allert message when no accounts are available")];
        return NO;
    }
Andreas Traczyk's avatar
Andreas Traczyk committed
639
    if (convModel_->allFilteredConversations().size() <= 0) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
640 641
        return YES;
    }
Andreas Traczyk's avatar
Andreas Traczyk committed
642
    auto model = convModel_->filteredConversation(0);
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
643 644 645 646 647
    auto uid = model.uid;
    if (selectedUid_ == uid) {
        return YES;
    }
    @try {
Andreas Traczyk's avatar
Andreas Traczyk committed
648
        auto contact = convModel_->owner.contactModel->getContact(model.participants[0]);
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
649
        if ((contact.profileInfo.uri.empty() && contact.profileInfo.type != lrc::api::profile::Type::SIP) || contact.profileInfo.type == lrc::api::profile::Type::INVALID) {
650 651
            return YES;
        }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
652
        selectedUid_ = uid;
Andreas Traczyk's avatar
Andreas Traczyk committed
653
        convModel_->selectConversation(uid);
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
654 655 656 657
        [self.view.window makeFirstResponder: smartView];
        return YES;
    } @catch (NSException *exception) {
        return YES;
658 659 660
    }
}

661 662
- (void)controlTextDidChange:(NSNotification *) notification
{
663
    [self processSearchFieldInput];
664 665
}

666 667 668 669 670 671 672 673 674 675
#pragma mark - NSPopOverDelegate

- (void)popoverDidClose:(NSNotification *)notification
{
    if (addToContactPopover != nullptr) {
        [addToContactPopover performClose:self];
        addToContactPopover = NULL;
    }
}

Alexandre Lision's avatar
Alexandre Lision committed
676

677 678 679 680 681 682 683 684 685 686 687 688 689 690
#pragma mark - ContactLinkedDelegate

- (void)contactLinked
{
    if (addToContactPopover != nullptr) {
        [addToContactPopover performClose:self];
        addToContactPopover = NULL;
    }
}

#pragma mark - KeyboardShortcutDelegate

- (void) onAddShortcut
{
691
    if ([smartView selectedRow] == -1)
692
        return;
Andreas Traczyk's avatar
Andreas Traczyk committed
693
    if (convModel_ == nil)
694
        return ;
695

Andreas Traczyk's avatar
Andreas Traczyk committed
696 697
    auto uid = convModel_->filteredConversation([smartView selectedRow]).uid;
    convModel_->makePermanent(uid);
698 699 700 701
}

#pragma mark - ContextMenuDelegate

702
- (NSMenu*) contextualMenuForRow:(int) index
703
{
Andreas Traczyk's avatar
Andreas Traczyk committed
704
    if (convModel_ == nil)
705 706
        return nil;

Andreas Traczyk's avatar
Andreas Traczyk committed
707
    auto conversation = convModel_->filteredConversation(NSInteger(index));
708

709
    @try {
Andreas Traczyk's avatar
Andreas Traczyk committed
710
        auto contact = convModel_->owner.contactModel->getContact(conversation.participants[0]);
711 712 713
        if (contact.profileInfo.type == lrc::api::profile::Type::INVALID) {
            return nil;
        }
714

715 716 717 718 719 720 721 722 723 724 725 726 727
        BOOL isSIP = false;
        BOOL isRingContact = false;
        /* for SIP contact show only call menu options
         * if contact does not have uri that is not RING contact
         * for trusted Ring contact show option block contact
         * for untrasted contact show option add contact
         */

        if (contact.profileInfo.type == lrc::api::profile::Type::SIP) {
            isSIP = true;
        } else if (contact.profileInfo.uri.empty()) {
            return nil;
        }
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 768 769
        else if (contact.profileInfo.type == lrc::api::profile::Type::RING && contact.isTrusted == true) {
            isRingContact = true;
        }
        auto conversationUD = conversation.uid;
        NSMenu *theMenu = [[NSMenu alloc] initWithTitle:@""];
        NSString* conversationUID = @(conversationUD.c_str());
        NSMenuItem* separator = [NSMenuItem separatorItem];
        NSMenuItem* videoCallItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Place video call",
                                                                                        @"Contextual menu action")
                                                               action:@selector(videoCall:)
                                                        keyEquivalent:@""];
        NSMenuItem* audioCallItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Place audio call",
                                                                                        @"Contextual menu action")
                                                               action:@selector(audioCall:)
                                                        keyEquivalent:@""];
        [videoCallItem setRepresentedObject: conversationUID];
        [audioCallItem setRepresentedObject: conversationUID];
        [theMenu addItem:videoCallItem];
        [theMenu addItem:audioCallItem];
        if (isSIP == false) {
            [theMenu addItem:separator];
            NSMenuItem* clearConversationItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Clear conversation", @"Contextual menu action")
                                                                           action:@selector(clearConversation:)
                                                                    keyEquivalent:@""];
            [clearConversationItem setRepresentedObject: conversationUID];
            [theMenu addItem:clearConversationItem];
            if(isRingContact) {
                NSMenuItem* blockContactItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Block contact", @"Contextual menu action")
                                                                          action:@selector(blockContact:)
                                                                   keyEquivalent:@""];
                [blockContactItem setRepresentedObject: conversationUID];
                [theMenu addItem:blockContactItem];
            } else {
                NSMenuItem* addContactItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Add to contacts", @"Contextual menu action")
                                                                        action:@selector(addContact:)
                                                                 keyEquivalent:@"A"];
                [addContactItem setRepresentedObject: conversationUID];
                [theMenu addItem:addContactItem];
            }
        }
        return theMenu;
770
    }
771 772 773 774
    @catch (NSException *exception) {
        return nil;
    }
}
775

776 777 778 779 780 781 782
- (void) addContact: (NSMenuItem* ) item  {
    auto menuObject = item.representedObject;
    if(menuObject == nil) {
        return;
    }
    NSString * convUId = (NSString*)menuObject;
    std::string conversationID = std::string([convUId UTF8String]);
Andreas Traczyk's avatar
Andreas Traczyk committed
783
    convModel_->makePermanent(conversationID);
784
}
785

786 787 788 789
- (void) blockContact: (NSMenuItem* ) item  {
    auto menuObject = item.representedObject;
    if(menuObject == nil) {
        return;
790
    }
791 792
    NSString * convUId = (NSString*)menuObject;
    std::string conversationID = std::string([convUId UTF8String]);
Andreas Traczyk's avatar
Andreas Traczyk committed
793 794
    //convModel_->clearHistory(conversationID);
    convModel_->removeConversation(conversationID, true);
795
}
796

797 798 799 800 801 802 803
- (void) audioCall: (NSMenuItem* ) item  {
    auto menuObject = item.representedObject;
    if(menuObject == nil) {
        return;
    }
    NSString * convUId = (NSString*)menuObject;
    std::string conversationID = std::string([convUId UTF8String]);
Andreas Traczyk's avatar
Andreas Traczyk committed
804
    convModel_->placeAudioOnlyCall(conversationID);
805

806
}
807

808 809 810 811 812 813 814
- (void) videoCall: (NSMenuItem* ) item  {
    auto menuObject = item.representedObject;
    if(menuObject == nil) {
        return;
    }
    NSString * convUId = (NSString*)menuObject;
    std::string conversationID = std::string([convUId UTF8String]);
Andreas Traczyk's avatar
Andreas Traczyk committed
815
    convModel_->placeCall(conversationID);
816
}
817

818 819 820 821 822 823 824
- (void) clearConversation:(NSMenuItem* ) item  {
    auto menuObject = item.representedObject;
    if(menuObject == nil) {
        return;
    }
    NSString * convUId = (NSString*)menuObject;
    std::string conversationID = std::string([convUId UTF8String]);
Andreas Traczyk's avatar
Andreas Traczyk committed
825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875
    convModel_->clearHistory(conversationID);
}

- (void)acceptInvitation:(id)sender {
    NSInteger row = [smartView rowForView:sender];

    if (row == -1)
        return;
    if (convModel_ == nil)
        return;

    auto conv = convModel_->filteredConversation(row);
    auto& convID = conv.Info::uid;

    if (convID.empty())
        return;
    convModel_->makePermanent(convID);
}

- (void)refuseInvitation:(id)sender {
    NSInteger row = [smartView rowForView:sender];

    if (row == -1)
        return;
    if (convModel_ == nil)
        return;

    auto conv = convModel_->filteredConversation(row);
    auto& convID = conv.Info::uid;

    if (convID.empty())
        return;
    convModel_->removeConversation(convID);
}

- (void)blockPendingContact:(id)sender {
    NSInteger row = [smartView rowForView:sender];

    if (row == -1)
        return;
    if (convModel_ == nil)
        return;

    auto conv = convModel_->filteredConversation(row);
    auto& convID = conv.Info::uid;

    if (convID.empty())
        return;
    convModel_->removeConversation(convID, true);
    [self deselect];
    [delegate listTypeChanged];
876 877
}

878
@end