Commit e7d62ed1 authored by Anthony Léonard's avatar Anthony Léonard Committed by Olivier SOLDANO

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: default avatarOlivier Soldano <olivier.soldano@savoirfairelinux.com>
parent f79fd7ca
......@@ -313,7 +313,8 @@ ${CMAKE_CURRENT_SOURCE_DIR}/data/dark/video.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_delete.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/qrcode.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_action_video.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/pending_contact_request.png)
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/pending_contact_request.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_file_upload.png)
SET_SOURCE_FILES_PROPERTIES(${ring_ICONS} PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
......
/*
* Copyright (C) 2016-2017 Savoir-faire Linux Inc.
* Copyright (C) 2016-2018 Savoir-faire Linux Inc.
* Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
*
......@@ -52,6 +52,7 @@
__unsafe_unretained IBOutlet NSTextField* conversationTitle;
__unsafe_unretained IBOutlet NSTextField *conversationID;
__unsafe_unretained IBOutlet IconButton* sendButton;
__unsafe_unretained IBOutlet IconButton *sendFileButton;
__unsafe_unretained IBOutlet NSLayoutConstraint* sentContactRequestWidth;
__unsafe_unretained IBOutlet NSButton* sentContactRequestButton;
IBOutlet MessagesVC* messagesViewVC;
......@@ -149,6 +150,7 @@
NSString* bestId = bestIDForConversation(*conv, *convModel_);
[conversationTitle setStringValue: bestName];
[conversationID setStringValue: bestId];
[sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
BOOL hideBestId = [bestNameForConversation(*conv, *convModel_) isEqualTo:bestIDForConversation(*conv, *convModel_)];
......@@ -200,6 +202,25 @@
}
}
- (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_) {
convModel_->sendFile(convUid_, std::string(fullPath), std::string([fileName UTF8String]));
}
}
}
}
- (IBAction)placeCall:(id)sender
{
auto* conv = [self getCurrentConversation];
......
/*
* Copyright (C) 2015-2017 Savoir-faire Linux Inc.
* Copyright (C) 2015-2018 Savoir-faire Linux Inc.
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
......@@ -29,7 +29,7 @@
@interface MessagesVC : NSViewController
-(void)setConversationUid:(const std::string)convUid model:(const lrc::api::ConversationModel*)model;
-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel*)model;
-(void)newMessageSent;
@property (retain, nonatomic) id <MessagesVCDelegate> delegate;
......
/*
* 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
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
* Copyright (C) 2016-2018 Savoir-faire Linux Inc.
* Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
*
* 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
......@@ -25,8 +26,14 @@
@property (nonatomic, strong) IBOutlet NSImageView* photoView;
@property (nonatomic, strong) IBOutlet NSTextView* msgView;
@property (nonatomic, strong) IBOutlet MessageBubbleView* msgBackground;
@property (nonatomic, strong) IBOutlet NSButton* acceptButton;
@property (nonatomic, strong) IBOutlet NSButton* declineButton;
@property (nonatomic, strong) IBOutlet NSProgressIndicator* progressIndicator;
@property (nonatomic, strong) IBOutlet NSTextField* statusLabel;
- (void) setup;
- (uint64_t) interaction;
- (void) setupForInteraction:(uint64_t)inter;
- (void) updateWidthConstraint:(CGFloat) newWidth;
@end
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
* Copyright (C) 2016-2018 Savoir-faire Linux Inc.
* Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
*
* 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
......@@ -18,25 +19,35 @@
*/
#import "IMTableCellView.h"
#import "NSColor+RingTheme.h"
@implementation IMTableCellView
@implementation IMTableCellView {
uint64_t interaction;
}
@synthesize msgView;
@synthesize photoView;
@synthesize acceptButton;
@synthesize declineButton;
@synthesize progressIndicator;
@synthesize statusLabel;
- (void) setup
- (void) setupDirection
{
if ([self.identifier isEqualToString:@"RightMessageView"]) {
if ([self.identifier containsString:@"Right"]) {
self.msgBackground.pointerDirection = RIGHT;
self.msgBackground.bgColor = [NSColor ringLightBlue];
}
else {
self.msgBackground.pointerDirection = LEFT;
self.msgBackground.bgColor = [NSColor whiteColor];
}
}
- (void) setupForInteraction:(uint64_t)inter
{
interaction = inter;
[self setupDirection];
[self.msgView setBackgroundColor:[NSColor clearColor]];
[self.msgView setString:@""];
[self.msgView setAutoresizingMask:NSViewWidthSizable];
......@@ -45,11 +56,14 @@
[self.msgBackground setAutoresizingMask:NSViewHeightSizable];
[self.msgView setEnabledTextCheckingTypes:NSTextCheckingTypeLink];
[self.msgView setAutomaticLinkDetectionEnabled:YES];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-5-[msgView]"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(msgView)]];
}
[self.msgView setEditable:NO];
if ([self.identifier containsString:@"Message"]) {
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-5-[msgView]"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(msgView)]];
}
}
- (void) updateWidthConstraint:(CGFloat) newWidth
{
......@@ -66,4 +80,9 @@
[self.msgView addConstraint:constraint];
}
- (uint64_t)interaction
{
return interaction;
}
@end
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment