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 <kateryna.kostiuk@savoirfairelinux.com>
diff --git a/src/MessagesVC.mm b/src/MessagesVC.mm
index a44ba20..035c4f8 100644
--- a/src/MessagesVC.mm
+++ b/src/MessagesVC.mm
@@ -1,6 +1,7 @@
/*
* 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;
- _selectionManager = [[INDSequentialTextSelectionManager alloc] init];
+ if (cachedConv_ != nil)
+ return cachedConv_;
- [self.selectionManager unregisterAllTextViews];
+ auto& convQueue = convModel_->allFilteredConversations();
- 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];
+ 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:(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;
}
-- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
+- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
- return YES;
+ return NO;
}
-- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
+- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
- QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
- if(!qIdx.isValid()) {
- return [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self];
+ auto* conv = [self getCurrentConversation];
+
+ if (conv == nil)
+ return nil;
+
+ // 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.
+ // 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 (msgCount == row) {
+ return true;
+ } else {
+ msgCount++;
+ return false;
+ }
+ }
+ return false;
+ });
}
- auto dir = qvariant_cast<Media::Media::Direction>(qIdx.data((int)Media::TextRecording::Role::Direction));
+
+ if (it == conv->interactions.end())
+ return nil;
+
IMTableCellView* result;
- if (dir == Media::Media::Direction::IN) {
- result = [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self];
+ 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 = [outlineView makeViewWithIdentifier:@"RightMessageView" owner:self];
+ result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
}
// check if the message first in incoming or outgoing messages sequence
Boolean isFirstInSequence = true;
- int row = qIdx.row() - 1;
- if(row >= 0) {
- QModelIndex index = currentModel->index(row, 0);
- if(index.isValid()) {
- auto dirOld = qvariant_cast<Media::Media::Direction>(index.data((int)Media::TextRecording::Role::Direction));
- isFirstInSequence = !(dirOld == dir);
- }
+ if (it != conv->interactions.begin()) {
+ auto previousIt = it;
+ previousIt--;
+ auto& previousInteraction = previousIt->second;
+ if (previousInteraction.type == lrc::api::interaction::Type::TEXT && (isOutgoing == lrc::api::interaction::isOutgoing(previousInteraction)))
+ isFirstInSequence = false;
}
[result.photoView setHidden:!isFirstInSequence];
result.msgBackground.needPointer = isFirstInSequence;
[result setup];
NSMutableAttributedString* msgAttString =
- [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()]
+ [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
attributes:[self messageAttributes]];
+ NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
NSAttributedString* timestampAttrString =
- [[NSAttributedString alloc] initWithString:qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString()
+ [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
attributes:[self timestampAttributes]];
-
CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width);
- finalWidth = MIN(finalWidth + 30, outlineView.frame.size.width * 0.7);
-
- NSString* msgString = qIdx.data((int)Qt::DisplayRole).toString().toNSString();
- NSString* dateString = qIdx.data((int)Qt::DisplayRole).toString().toNSString();
+ finalWidth = MIN(finalWidth + 30, tableView.frame.size.width * 0.7);
[msgAttString appendAttributedString:timestampAttrString];
[[result.msgView textStorage] appendAttributedString:msgAttString];
[result.msgView checkTextInDocument:nil];
[result updateWidthConstraint:finalWidth];
- [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(qIdx.data(Qt::DecorationRole)))];
+
+ auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
+ [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
return result;
}
-- (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
+- (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
{
- if (IMTableCellView* cellView = [outlineView viewAtColumn:0 row:row makeIfNecessary:NO]) {
+ if (IMTableCellView* cellView = [tableView viewAtColumn:0 row:row makeIfNecessary:NO]) {
[self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue];
}
- [self.delegate newMessageAdded];
}
-- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
+- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
- QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
- double someWidth = outlineView.frame.size.width * 0.7;
+ double someWidth = tableView.frame.size.width * 0.7;
- NSMutableAttributedString* msgAttString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()]
- attributes:[self messageAttributes]];
- NSAttributedString *timestampAttrString = [[NSAttributedString alloc] initWithString:
- qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString()
- attributes:[self timestampAttributes]];
+ auto* conv = [self getCurrentConversation];
+
+ if (conv == nil)
+ return 0;
+
+ // 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.
+ // 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 (msgCount == row) {
+ return true;
+ } else {
+ msgCount++;
+ return false;
+ }
+ }
+ return false;
+ });
+ }
+
+ if (it == conv->interactions.end())
+ return 0;
+
+ auto& interaction = it->second;
+
+ // TODO Implement interactions other than messages
+ if(interaction.type != lrc::api::interaction::Type::TEXT) {
+ return 0;
+ }
+
+ NSMutableAttributedString* msgAttString =
+ [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
+ attributes:[self messageAttributes]];
+
+ NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
+ NSAttributedString* timestampAttrString =
+ [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
+ attributes:[self timestampAttributes]];
+
+ [msgAttString appendAttributedString:timestampAttrString];
[msgAttString appendAttributedString:timestampAttrString];
@@ -161,6 +282,29 @@
return MAX(height, 50.0f);
}
+#pragma mark - NSTableViewDataSource
+
+- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
+{
+ auto* conv = [self getCurrentConversation];
+
+ 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 count;
+ }
+ return 0;
+
+#if 0
+ // TODO: Replace above code by the following one when every interaction types implemented
+ if (conv_) {
+ return conv_->interactions.size();
+ }
+#endif
+}
+
#pragma mark - Text formatting
- (NSMutableDictionary*) timestampAttributes