add file transfer in conversations

- Add a send file button in conversation view
- Add new message types in chat view with buttons to control file
  transfer interactions (accept, cancel, status, etc.)
- When receiving a file, a dialog is presented to chose a location.
  It is meant to be replaced by a settable default location.
- An animation is displayed during transfer. It will be replaced by a
  progress bar when LRC side implemented.

Change-Id: I2ea0210823d697f4ada75a33b720d63288b36983
Reviewed-by: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
diff --git a/src/MessagesVC.mm b/src/MessagesVC.mm
index 84f0659..b771330 100644
--- a/src/MessagesVC.mm
+++ b/src/MessagesVC.mm
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2015-2017 Savoir-faire Linux Inc.
+ *  Copyright (C) 2015-2018 Savoir-faire Linux Inc.
  *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
  *          Anthony Léonard <anthony.leonard@savoirfairelinux.com>
  *
@@ -38,15 +38,16 @@
     __unsafe_unretained IBOutlet NSTableView* conversationView;
 
     std::string convUid_;
-    const lrc::api::ConversationModel* convModel_;
+    lrc::api::ConversationModel* convModel_;
     const lrc::api::conversation::Info* cachedConv_;
 
-    QMetaObject::Connection newMessageSignal_;
+    QMetaObject::Connection newInteractionSignal_;
 
     // Both are needed to invalidate cached conversation as pointer
     // may not be referencing the same conversation anymore
     QMetaObject::Connection modelSortedSignal_;
     QMetaObject::Connection filterChangedSignal_;
+    QMetaObject::Connection interactionStatusUpdatedSignal_;
 }
 
 @property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
@@ -73,7 +74,7 @@
     return cachedConv_;
 }
 
--(void)setConversationUid:(const std::string)convUid model:(const lrc::api::ConversationModel *)model
+-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
 {
     if (convUid_ == convUid && convModel_ == model)
         return;
@@ -82,15 +83,25 @@
     convUid_ = convUid;
     convModel_ = model;
 
-    // Signal triggered when messages are received
-    QObject::disconnect(newMessageSignal_);
-    newMessageSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
+    // Signal triggered when messages are received or their status updated
+    QObject::disconnect(newInteractionSignal_);
+    QObject::disconnect(interactionStatusUpdatedSignal_);
+    newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
                                          [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
                                              if (uid != convUid_)
                                                  return;
+                                             cachedConv_ = nil;
                                              [conversationView reloadData];
                                              [conversationView scrollToEndOfDocument:nil];
                                          });
+    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;
+                                                           [conversationView reloadData];
+                                                           [conversationView scrollToEndOfDocument:nil];
+                                                       });
 
     // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
     // after a reordering.
@@ -117,6 +128,57 @@
     [conversationView scrollToEndOfDocument:nil];
 }
 
+-(IMTableCellView*) makeViewforTransferStatus:(lrc::api::interaction::Status)status type:(lrc::api::interaction::Type)type tableView:(NSTableView*)tableView
+{
+    IMTableCellView* result;
+
+    // First, view is created
+    if (type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
+        switch (status) {
+            case lrc::api::interaction::Status::TRANSFER_CREATED:
+            case lrc::api::interaction::Status::TRANSFER_AWAITING:
+                result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner:self];
+                break;
+            case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
+            case lrc::api::interaction::Status::TRANSFER_ONGOING:
+                result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:self];
+                [result.progressIndicator startAnimation:nil];
+                break;
+            case lrc::api::interaction::Status::TRANSFER_FINISHED:
+            case lrc::api::interaction::Status::TRANSFER_CANCELED:
+            case lrc::api::interaction::Status::TRANSFER_ERROR:
+                result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:self];
+        }
+    } else if (type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
+        switch (status) {
+            case lrc::api::interaction::Status::TRANSFER_CREATED:
+            case lrc::api::interaction::Status::TRANSFER_AWAITING:
+            case lrc::api::interaction::Status::TRANSFER_ONGOING:
+            case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
+                result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:self];
+                [result.progressIndicator startAnimation:nil];
+                break;
+            case lrc::api::interaction::Status::TRANSFER_FINISHED:
+            case lrc::api::interaction::Status::TRANSFER_CANCELED:
+            case lrc::api::interaction::Status::TRANSFER_ERROR:
+                result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:self];
+        }
+    }
+
+    // Then status label is updated if needed
+    switch (status) {
+        case lrc::api::interaction::Status::TRANSFER_FINISHED:
+            [result.statusLabel setStringValue:@"Success"];
+            break;
+        case lrc::api::interaction::Status::TRANSFER_CANCELED:
+            [result.statusLabel setStringValue:@"Canceled"];
+            break;
+        case lrc::api::interaction::Status::TRANSFER_ERROR:
+            [result.statusLabel setStringValue:@"Failed"];
+    }
+    return result;
+}
+
 #pragma mark - NSTableViewDelegate methods
 - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
 {
@@ -137,14 +199,16 @@
 
     // HACK HACK HACK HACK HACK
     // The following code has to be replaced when every views are implemented for every interaction types
-    // This is an iterator which "jumps over" any interaction which is not a text one.
+    // This is an iterator which "jumps over" any interaction which is not a text or datatransfer one.
     // It behaves as if interaction list was only containing text interactions.
     std::map<uint64_t, lrc::api::interaction::Info>::const_iterator it;
 
     {
         int msgCount = 0;
         it = std::find_if(conv->interactions.begin(), conv->interactions.end(), [&msgCount, row](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
-            if (inter.second.type == lrc::api::interaction::Type::TEXT) {
+            if (inter.second.type == lrc::api::interaction::Type::TEXT
+                || inter.second.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER
+                || inter.second.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
                 if (msgCount == row) {
                     return true;
                 } else {
@@ -163,17 +227,22 @@
 
     auto& interaction = it->second;
 
-    // TODO Implement interactions other than messages
-    if(interaction.type != lrc::api::interaction::Type::TEXT) {
-        return nil;
-    }
-
     bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
 
-    if (isOutgoing) {
-        result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
-    } else {
-        result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
+    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:
+            result = [self makeViewforTransferStatus:interaction.status type:interaction.type tableView:tableView];
+            break;
+        default:  // If interaction is not of a known type
+            return nil;
     }
 
     // check if the message first in incoming or outgoing messages sequence
@@ -182,12 +251,14 @@
         auto previousIt = it;
         previousIt--;
         auto& previousInteraction = previousIt->second;
-        if (previousInteraction.type == lrc::api::interaction::Type::TEXT && (isOutgoing == lrc::api::interaction::isOutgoing(previousInteraction)))
+        if ((previousInteraction.type == lrc::api::interaction::Type::TEXT
+             || previousInteraction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER
+             || previousInteraction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) && (isOutgoing == lrc::api::interaction::isOutgoing(previousInteraction)))
             isFirstInSequence = false;
     }
     [result.photoView setHidden:!isFirstInSequence];
     result.msgBackground.needPointer = isFirstInSequence;
-    [result setup];
+    [result setupForInteraction:it->first];
 
     NSMutableAttributedString* msgAttString =
     [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
@@ -235,14 +306,16 @@
 
     // HACK HACK HACK HACK HACK
     // The following code has to be replaced when every views are implemented for every interaction types
-    // This is an iterator which "jumps over" any interaction which is not a text one.
+    // This is an iterator which "jumps over" any interaction which is not a text or datatransfer one.
     // It behaves as if interaction list was only containing text interactions.
     std::map<uint64_t, lrc::api::interaction::Info>::const_iterator it;
 
     {
         int msgCount = 0;
         it = std::find_if(conv->interactions.begin(), conv->interactions.end(), [&msgCount, row](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
-            if (inter.second.type == lrc::api::interaction::Type::TEXT) {
+            if (inter.second.type == lrc::api::interaction::Type::TEXT
+                || inter.second.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER
+                || inter.second.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
                 if (msgCount == row) {
                     return true;
                 } else {
@@ -259,6 +332,9 @@
 
     auto& interaction = it->second;
 
+    if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER)
+        return 52.0;
+
     // TODO Implement interactions other than messages
     if(interaction.type != lrc::api::interaction::Type::TEXT) {
         return 0;
@@ -297,8 +373,11 @@
     if (conv) {
         int count;
         count = std::count_if(conv->interactions.begin(), conv->interactions.end(), [](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
-            return inter.second.type == lrc::api::interaction::Type::TEXT;
+            return inter.second.type == lrc::api::interaction::Type::TEXT
+            || inter.second.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER
+            || inter.second.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER;
         });
+        NSLog(@"$$$ Interaction count: %d", count);
         return count;
     }
     return 0;
@@ -361,4 +440,27 @@
      return aMutableParagraphStyle;
 }
 
+#pragma mark - Actions
+
+- (IBAction)acceptIncomingFile:(id)sender {
+    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);
+        }
+    }
+}
+
+- (IBAction)declineIncomingFile:(id)sender {
+    auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
+    if (convModel_ && !convUid_.empty()) {
+        convModel_->cancelTransfer(convUid_, inter);
+    }
+}
+
 @end