Commit 2382b56e authored by Anthony Léonard's avatar Anthony Léonard Committed by Kateryna Kostiuk

refactoring of messaging controller with new model

MessagesVC is now implemented using the new LRC model for
conversations.
 - Both views to display the messages (in call and off call)
   initialize their MessagesVC with the current conversation when
   needed.
 - A conversation caching system is introduced to not get the whole
   conversation::Info structure from LRC at each display request (once
   per message).

Change-Id: Ib520c1f88be78de37968d3d7741010f2c73f20ea
Reviewed-by: Kateryna Kostiuk's avatarKateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
parent be339114
......@@ -17,6 +17,8 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#import <Cocoa/Cocoa.h>
#import <api/conversation.h>
#import <api/conversationmodel.h>
@interface ChatVC : NSViewController <NSTextFieldDelegate>
......@@ -27,6 +29,7 @@
*/
@property (retain) NSString* message;
- (void) setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel*)model;
- (void) takeFocus;
@end
/*
* Copyright (C) 2015-2016 Savoir-faire Linux Inc.
* Copyright (C) 2015-2017 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
......@@ -19,45 +20,25 @@
#import "ChatVC.h"
#import <QItemSelectionModel>
#import <qstring.h>
#import <media/media.h>
#import <media/text.h>
#import <media/textrecording.h>
#import <callmodel.h>
#import "MessagesVC.h"
@interface MediaConnectionsHolder : NSObject
@property QMetaObject::Connection newMediaAdded;
@property QMetaObject::Connection newMessage;
@end
@implementation MediaConnectionsHolder
@end
@interface ChatVC () <MessagesVCDelegate>
@interface ChatVC ()
{
IBOutlet MessagesVC* messagesViewVC;
IBOutlet MessagesVC* messagesViewVC;
std::string convUid_;
lrc::api::ConversationModel* convModel_;
}
@property (unsafe_unretained) IBOutlet NSTextField *messageField;
@property (unsafe_unretained) IBOutlet NSButton *sendButton;
@property MediaConnectionsHolder* mediaHolder;
@end
@implementation ChatVC
@synthesize messageField,sendButton, mediaHolder;
@synthesize messageField,sendButton;
- (void)awakeFromNib
{
......@@ -66,73 +47,14 @@
[self.view setWantsLayer:YES];
[self.view setLayer:[CALayer layer]];
[self.view.layer setBackgroundColor:[NSColor controlColor].CGColor];
mediaHolder = [[MediaConnectionsHolder alloc] init];
QObject::connect(CallModel::instance().selectionModel(),
&QItemSelectionModel::currentChanged,
[=](const QModelIndex &current, const QModelIndex &previous) {
[self setupChat];
});
messagesViewVC.delegate = self;
}
- (void) setupChat
-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
{
QObject::disconnect(mediaHolder.newMediaAdded);
QObject::disconnect(mediaHolder.newMessage);
QModelIndex callIdx = CallModel::instance().selectionModel()->currentIndex();
if (!callIdx.isValid())
return;
Call* call = CallModel::instance().getCall(callIdx);
/* check if text media is already present */
if (call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::IN)) {
Media::Text *text = call->firstMedia<Media::Text>(Media::Media::Direction::IN);
[self parseChatModel:text->recording()->instantMessagingModel()];
} else if (call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::OUT)) {
Media::Text *text = call->firstMedia<Media::Text>(Media::Media::Direction::OUT);
[self parseChatModel:text->recording()->instantMessagingModel()];
} else {
/* monitor media for messaging text messaging */
mediaHolder.newMediaAdded = QObject::connect(call,
&Call::mediaAdded,
[self] (Media::Media* media) {
if (media->type() == Media::Media::Type::TEXT) {
QObject::disconnect(mediaHolder.newMediaAdded);
[self parseChatModel:((Media::Text*)media)->recording()->instantMessagingModel()];
}
});
}
}
#pragma mark - MessagesVC delegate
-(void) newMessageAdded {
QModelIndex callIdx = CallModel::instance().selectionModel()->currentIndex();
if (!callIdx.isValid())
return;
Call* call = CallModel::instance().getCall(callIdx);
if (call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::IN)) {
Media::Text *text = call->firstMedia<Media::Text>(Media::Media::Direction::IN);
auto textRecording = text->recording();
textRecording->setAllRead();
} else if (call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::OUT)) {
Media::Text *text = call->firstMedia<Media::Text>(Media::Media::Direction::OUT);
auto textRecording = text->recording();
textRecording->setAllRead();
}
}
- (void) parseChatModel:(QAbstractItemModel *)model
convUid_ = convUid;
convModel_ = model;
{
[messagesViewVC setUpViewWithModel:model];
[messagesViewVC setConversationUid:convUid_ model:convModel_];
}
- (void) takeFocus
......@@ -141,19 +63,13 @@
}
- (IBAction)sendMessage:(id)sender {
QModelIndex callIdx = CallModel::instance().selectionModel()->currentIndex();
Call* call = CallModel::instance().getCall(callIdx);
/* make sure there is text to send */
NSString* text = self.message;
if (text && text.length > 0) {
QMap<QString, QString> messages;
messages["text/plain"] = QString::fromNSString(text);
call->addOutgoingMedia<Media::Text>()->send(messages);
// Empty the text after sending it
[self.messageField setStringValue:@""];
convModel_->sendMessage(convUid_, std::string([text UTF8String]));
self.message = @"";
[messageField setStringValue:@""];
[messagesViewVC newMessageSent];
}
}
......
......@@ -18,6 +18,8 @@
*/
#import <Cocoa/Cocoa.h>
#import <api/conversation.h>
#import <api/conversationmodel.h>
@interface ConversationVC : NSViewController
......@@ -32,4 +34,6 @@
*/
@property (retain) NSString* message;
- (void) setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel*)model;
@end
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
* Copyright (C) 2016-2017 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
......@@ -24,13 +25,7 @@
#import <QPixmap>
#import <QtMacExtras/qmacfunctions.h>
#import <media/media.h>
#import <recentmodel.h>
#import <person.h>
#import <contactmethod.h>
#import <media/text.h>
#import <media/textrecording.h>
#import <callmodel.h>
// LRC
#import <globalinstances.h>
#import "views/IconButton.h"
......@@ -43,22 +38,17 @@
#import "account.h"
#import "AvailableAccountModel.h"
#import "MessagesVC.h"
#import "utils.h"
#import <QuartzCore/QuartzCore.h>
@interface ConversationVC () <NSOutlineViewDelegate, MessagesVCDelegate> {
@interface ConversationVC () {
__unsafe_unretained IBOutlet NSTextField* messageField;
QVector<ContactMethod*> contactMethods;
NSMutableString* textSelection;
QMetaObject::Connection contactMethodChanged;
ContactMethod* selectedContactMethod;
__unsafe_unretained IBOutlet NSView* sendPanel;
__unsafe_unretained IBOutlet NSTextField* conversationTitle;
__unsafe_unretained IBOutlet NSTextField* emptyConversationPlaceHolder;
__unsafe_unretained IBOutlet IconButton* sendButton;
__unsafe_unretained IBOutlet NSPopUpButton* contactMethodsPopupButton;
__unsafe_unretained IBOutlet NSLayoutConstraint* sentContactRequestWidth;
......@@ -67,6 +57,15 @@
IBOutlet NSLayoutConstraint* titleHoverButtonConstraint;
IBOutlet NSLayoutConstraint* titleTopConstraint;
std::string convUid_;
const lrc::api::conversation::Info* cachedConv_;
lrc::api::ConversationModel* convModel_;
// Both are needed to invalidate cached conversation as pointer
// may not be referencing the same conversation anymore
QMetaObject::Connection modelSortedSignal_;
QMetaObject::Connection filterChangedSignal_;
}
......@@ -74,6 +73,68 @@
@implementation ConversationVC
-(const lrc::api::conversation::Info*) getCurrentConversation
{
if (convModel_ == nil || convUid_.empty())
return nil;
if (cachedConv_ != nil)
return cachedConv_;
auto& convQueue = convModel_->allFilteredConversations();
auto it = std::find_if(convQueue.begin(), convQueue.end(), [self](const lrc::api::conversation::Info& conv) {return conv.uid == convUid_;});
if (it != convQueue.end())
cachedConv_ = &(*it);
return cachedConv_;
}
-(void) setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model {
if (convUid_ == convUid && convModel_ == model)
return;
cachedConv_ = nil;
convUid_ = convUid;
convModel_ = model;
[messagesViewVC setConversationUid:convUid_ model:convModel_];
if (convUid_.empty() || convModel_ == nil)
return;
// 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;
});
auto* conv = [self getCurrentConversation];
if (conv == nil)
return;
// Setup UI elements according to new conversation
NSString* bestName = bestNameForConversation(*conv, *convModel_);
[conversationTitle setStringValue: bestName];
[contactMethodsPopupButton setEnabled:NO];
[contactMethodsPopupButton setBordered:NO];
BOOL hideCMPopupButton = [bestNameForConversation(*conv, *convModel_) isEqualTo:bestIDForConversation(*conv, *convModel_)];
[contactMethodsPopupButton setHidden:hideCMPopupButton];
[titleHoverButtonConstraint setActive:hideCMPopupButton];
[titleTopConstraint setActive:!hideCMPopupButton];
}
- (void)loadView {
[super loadView];
// Do view setup here.
......@@ -83,9 +144,6 @@
[self.view.layer setCornerRadius:5.0f];
[messageField setFocusRingType:NSFocusRingTypeNone];
[self setupChat];
}
-(Account* ) chosenAccount
......@@ -105,80 +163,26 @@
self.view.layer.position = self.view.frame.origin;
}
- (void) setupChat
{
QObject::connect(RecentModel::instance().selectionModel(),
&QItemSelectionModel::currentChanged,
[=](const QModelIndex &current, const QModelIndex &previous) {
contactMethods = RecentModel::instance().getContactMethods(current);
if (contactMethods.isEmpty()) {
return ;
}
[contactMethodsPopupButton removeAllItems];
for (auto cm : contactMethods) {
[contactMethodsPopupButton addItemWithTitle:cm->bestId().toNSString()];
}
BOOL isSMultipleCM = (contactMethods.length() > 1);
BOOL hideCMPopupButton = !isSMultipleCM && (contactMethods.first()->bestId() == contactMethods.first()->bestName());
[contactMethodsPopupButton setEnabled:isSMultipleCM];
[contactMethodsPopupButton setBordered:isSMultipleCM];
[contactMethodsPopupButton setHidden:hideCMPopupButton];
[[contactMethodsPopupButton cell] setArrowPosition: !isSMultipleCM ? NSPopUpNoArrow : NSPopUpArrowAtBottom];
[titleHoverButtonConstraint setActive:hideCMPopupButton];
[titleTopConstraint setActive:!hideCMPopupButton];
[emptyConversationPlaceHolder setHidden:NO];
// Select first cm
[contactMethodsPopupButton selectItemAtIndex:0];
[self itemChanged:contactMethodsPopupButton];
});
}
- (IBAction)sendMessage:(id)sender
{
auto* conv = [self getCurrentConversation];
/* make sure there is text to send */
NSString* text = self.message;
if (text && text.length > 0) {
QMap<QString, QString> messages;
messages["text/plain"] = QString::fromNSString(text);
contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])->sendOfflineTextMessage(messages);
convModel_->sendMessage(conv->uid, std::string([text UTF8String]));
self.message = @"";
[messagesViewVC newMessageSent];
}
}
- (IBAction)placeCall:(id)sender
{
if(auto cm = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])) {
auto c = CallModel::instance().dialingCall();
c->setPeerContactMethod(cm);
c << Call::Action::ACCEPT;
CallModel::instance().selectCall(c);
}
auto* conv = [self getCurrentConversation];
convModel_->placeCall(conv->uid);
}
- (IBAction)backPressed:(id)sender {
RecentModel::instance().selectionModel()->clearCurrentIndex();
messagesViewVC.delegate = nil;
}
- (IBAction)sendContactRequest:(id)sender
{
auto cm = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem]);
if(cm) {
if(cm->account() == nullptr) {
cm->setAccount([self chosenAccount]);
}
if(cm->account() == nullptr) {
return;
}
cm->account()->sendContactRequest(cm);
}
[self animateOut];
}
# pragma mark private IN/OUT animations
......@@ -233,65 +237,5 @@
return NO;
}
-(BOOL)shouldHideSendRequestBtn {
/*to send contact request we need to meet thre condition:
1)contact method has RING protocol
2)accound is used to send request is also RING
3)contact have not acceppt request yet*/
if(selectedContactMethod->protocolHint() != URI::ProtocolHint::RING) {
return YES;
}
if(selectedContactMethod->isConfirmed()) {
return YES;
}
if(selectedContactMethod->account()) {
return selectedContactMethod->account()->protocol() != Account::Protocol::RING;
}
if([self chosenAccount]) {
return [self chosenAccount]->protocol() != Account::Protocol::RING;
}
return NO;
}
-(void)updateSendButtonVisibility
{
[sentContactRequestButton setHidden:[self shouldHideSendRequestBtn]];
sentContactRequestWidth.priority = [self shouldHideSendRequestBtn] ? 999: 250;
}
#pragma mark - NSPopUpButton item selection
- (IBAction)itemChanged:(id)sender {
NSInteger index = [(NSPopUpButton *)sender indexOfSelectedItem];
selectedContactMethod = contactMethods.at(index);
[self updateSendButtonVisibility];
[conversationTitle setStringValue:selectedContactMethod->bestName().toNSString()];
QObject::disconnect(contactMethodChanged);
contactMethodChanged = QObject::connect(selectedContactMethod,
&ContactMethod::changed,
[self] {
[conversationTitle setStringValue:selectedContactMethod->bestName().toNSString()];
[self updateSendButtonVisibility];
});
if (auto txtRecording = selectedContactMethod->textRecording()) {
messagesViewVC.delegate = self;
[messagesViewVC setUpViewWithModel:txtRecording->instantMessagingModel()];
[self.view.window makeFirstResponder:messageField];
}
}
#pragma mark - MessagesVC delegate
-(void) newMessageAdded {
if (auto txtRecording = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])->textRecording()) {
[emptyConversationPlaceHolder setHidden:txtRecording->instantMessagingModel()->rowCount() > 0];
txtRecording->setAllRead();
}
}
@end
......@@ -18,6 +18,8 @@
*/
#import <Cocoa/Cocoa.h>
#import <api/conversationmodel.h>
#import <api/conversation.h>
@protocol MessagesVCDelegate
......@@ -27,7 +29,9 @@
@interface MessagesVC : NSViewController
-(void)setUpViewWithModel: (QAbstractItemModel*) model;
-(void)setConversationUid:(const std::string)convUid model:(const lrc::api::ConversationModel*)model;
-(void)newMessageSent;
@property (retain, nonatomic) id <MessagesVCDelegate> delegate;
@end
/*
* Copyright (C) 2015-2017 Savoir-faire Linux Inc.
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
* 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,27 +19,33 @@
*/
#import <QItemSelectionModel>
#import <qstring.h>
#import <QPixmap>
#import <QtMacExtras/qmacfunctions.h>
#import <media/media.h>
#import <person.h>
#import <media/text.h>
#import <media/textrecording.h>
// LRC
#import <globalinstances.h>
#import <api/interaction.h>
#import "MessagesVC.h"
#import "QNSTreeController.h"
#import "views/IMTableCellView.h"
#import "views/MessageBubbleView.h"
#import "INDSequentialTextSelectionManager.h"
#import "delegates/ImageManipulationDelegate.h"
@interface MessagesVC () {
@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource> {
QNSTreeController* treeController;
__unsafe_unretained IBOutlet NSOutlineView* conversationView;
__unsafe_unretained IBOutlet NSTableView* conversationView;
std::string convUid_;
const lrc::api::ConversationModel* convModel_;
const lrc::api::conversation::Info* cachedConv_;
QMetaObject::Connection newMessageSignal_;
// Both are needed to invalidate cached conversation as pointer
// may not be referencing the same conversation anymore
QMetaObject::Connection modelSortedSignal_;
QMetaObject::Connection filterChangedSignal_;
}
@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
......@@ -46,107 +53,221 @@
@end
@implementation MessagesVC
QAbstractItemModel* currentModel;
-(void)setUpViewWithModel: (QAbstractItemModel*) model {
-(const lrc::api::conversation::Info*) getCurrentConversation
{
if (convModel_ == nil || convUid_.empty())
return nil;
if (cachedConv_ != nil)
return cachedConv_;
auto& convQueue = convModel_->allFilteredConversations();
_selectionManager = [[INDSequentialTextSelectionManager alloc] init];
auto it = std::find_if(convQueue.begin(), convQueue.end(), [self](const lrc::api::conversation::Info& conv) {return conv.uid == convUid_;});
[self.selectionManager unregisterAllTextViews];
if (it != convQueue.end())
cachedConv_ = &(*it);
treeController = [[QNSTreeController alloc] initWithQModel:model];
[treeController setAvoidsEmptySelection:NO];
[treeController setChildrenKeyPath:@"children"];
[conversationView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
[conversationView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
[conversationView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
return cachedConv_;
}
-(void)setConversationUid:(const std::string)convUid model:(const lrc::api::ConversationModel *)model
{
if (convUid_ == convUid && convModel_ == model)
return;
cachedConv_ = nil;
convUid_ = convUid;
convModel_ = model;
// Signal triggered when messages are received
QObject::disconnect(newMessageSignal_);
newMessageSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newUnreadMessage,
[self](const std::string& uid, uint64_t msgId, const lrc::api::interaction::Info& msg){
if (uid != convUid_)
return;
[conversationView reloadData];
[conversationView scrollToEndOfDocument:nil];
});
// 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;
});
[conversationView reloadData];
[conversationView scrollToEndOfDocument:nil];
currentModel = model;
}
#pragma mark - NSOutlineViewDelegate methods
-(void)newMessageSent
{
[conversationView reloadData];
[conversationView scrollToEndOfDocument:nil];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
#pragma mark - NSTableViewDelegate methods
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
{
return YES;
}