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