Commit 2db8f477 authored by Alexandre Lision's avatar Alexandre Lision Committed by Guillaume Roguez

contacts: create or update contacts

Add ability to create a new contact with an unknow uri, or link it to an
existing contact.
This is presented in a popover, either from an history entry, or during
a call with an unknown URI.

Issue: #78236
Change-Id: I22fa416b9f5c7a6eceb6f2ea47bb30aa251cda54
parent bb5c2466
......@@ -109,7 +109,9 @@ SET(ringclient_CONTROLLERS
src/BitrateVC.mm
src/BitrateVC.h
src/ChatVC.mm
src/ChatVC.h)
src/ChatVC.h
src/PersonLinkerVC.mm
src/PersonLinkerVC.h)
SET(ringclient_BACKENDS
src/backends/AddressBookBackend.mm
......@@ -121,7 +123,9 @@ SET(ringclient_VIEWS
src/views/ITProgressIndicator.mm
src/views/ITProgressIndicator.h
src/views/PersonCell.mm
src/views/PersonCell.h)
src/views/PersonCell.h
src/views/RingOutlineView.mm
src/views/RingOutlineView.h)
SET(ringclient_OTHERS
src/main.mm
......@@ -147,7 +151,8 @@ SET(ringclient_XIBS
VideoPrefs
PreferencesScreen
RingWizard
CertificateWindow)
CertificateWindow
PersonLinker)
# Icons
# This part tells CMake where to find and install the file itself
......@@ -168,6 +173,7 @@ ${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_action_search.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_action_quality.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ancrage.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/audio.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_person_add.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/general.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/video.png
${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_action_video.png)
......
......@@ -41,9 +41,11 @@
#import <video/previewmanager.h>
#import <video/renderer.h>
#import <media/text.h>
#import <person.h>
#import "views/ITProgressIndicator.h"
#import "views/CallView.h"
#import "PersonLinkerVC.h"
@interface RendererConnectionsHolder : NSObject
......@@ -57,7 +59,7 @@
@end
@interface CurrentCallVC ()
@interface CurrentCallVC () <NSPopoverDelegate, ContactLinkedDelegate>
@property (unsafe_unretained) IBOutlet NSTextField *personLabel;
@property (unsafe_unretained) IBOutlet NSTextField *stateLabel;
......@@ -67,6 +69,7 @@
@property (unsafe_unretained) IBOutlet NSButton *pickUpButton;
@property (unsafe_unretained) IBOutlet NSButton *muteAudioButton;
@property (unsafe_unretained) IBOutlet NSButton *muteVideoButton;
@property (unsafe_unretained) IBOutlet NSButton *addContactButton;
@property (unsafe_unretained) IBOutlet ITProgressIndicator *loadingIndicator;
......@@ -76,6 +79,7 @@
@property (unsafe_unretained) IBOutlet NSButton *chatButton;
@property (strong) IBOutlet NSPopover *qualityPopOver;
@property (strong) NSPopover* addToContactPopover;
@property QHash<int, NSButton*> actionHash;
......@@ -132,9 +136,16 @@
-(void) updateCall
{
QModelIndex callIdx = CallModel::instance()->selectionModel()->currentIndex();
if (!callIdx.isValid()) {
return;
}
[personLabel setStringValue:callIdx.data(Qt::DisplayRole).toString().toNSString()];
[timeSpentLabel setStringValue:callIdx.data((int)Call::Role::Length).toString().toNSString()];
auto contactmethod = qvariant_cast<Call*>(callIdx.data(static_cast<int>(Call::Role::Object)))->peerContactMethod();
BOOL shouldShow = (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder());
[self.addContactButton setHidden:!shouldShow];
Call::State state = callIdx.data((int)Call::Role::State).value<Call::State>();
[loadingIndicator setHidden:YES];
[stateLabel setStringValue:callIdx.data((int)Call::Role::HumanStateName).toString().toNSString()];
......@@ -525,6 +536,27 @@
#pragma mark - Button methods
- (IBAction)addToContact:(NSButton*) sender {
auto contactmethod = CallModel::instance()->getCall(CallModel::instance()->selectionModel()->currentIndex())->peerContactMethod();
if (self.addToContactPopover != nullptr) {
[self.addToContactPopover performClose:self];
self.addToContactPopover = NULL;
} else if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
auto* editorVC = [[PersonLinkerVC alloc] initWithNibName:@"PersonLinker" bundle:nil];
[editorVC setMethodToLink:contactmethod];
[editorVC setContactLinkedDelegate:self];
self.addToContactPopover = [[NSPopover alloc] init];
[self.addToContactPopover setContentSize:editorVC.view.frame.size];
[self.addToContactPopover setContentViewController:editorVC];
[self.addToContactPopover setAnimates:YES];
[self.addToContactPopover setBehavior:NSPopoverBehaviorTransient];
[self.addToContactPopover setDelegate:self];
[self.addToContactPopover showRelativeToRect:sender.bounds ofView:sender preferredEdge:NSMaxXEdge];
}
}
- (IBAction)hangUp:(id)sender {
CallModel::instance()->getCall(CallModel::instance()->selectionModel()->currentIndex()) << Call::Action::REFUSE;
}
......@@ -568,6 +600,26 @@
[self.qualityPopOver showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxXEdge];
}
#pragma mark - NSPopOverDelegate
- (void)popoverDidClose:(NSNotification *)notification
{
if (self.addToContactPopover != nullptr) {
[self.addToContactPopover performClose:self];
self.addToContactPopover = NULL;
}
}
#pragma mark - ContactLinkedDelegate
- (void)contactLinked
{
if (self.addToContactPopover != nullptr) {
[self.addToContactPopover performClose:self];
self.addToContactPopover = NULL;
}
}
#pragma mark - NSSplitViewDelegate
/* Return YES if the subview should be collapsed because the user has double-clicked on an adjacent divider. If a split view has a delegate, and the delegate responds to this message, it will be sent once for the subview before a divider when the user double-clicks on that divider, and again for the subview after the divider, but only if the delegate returned YES when sent -splitView:canCollapseSubview: for the subview in question. When the delegate indicates that both subviews should be collapsed NSSplitView's behavior is undefined.
......
......@@ -31,8 +31,9 @@
#define HISTORYVIEWCONTROLLER_H
#import <Cocoa/Cocoa.h>
#import "views/RingOutlineView.h"
@interface HistoryVC : NSViewController <NSOutlineViewDelegate> {
@interface HistoryVC : NSViewController <NSOutlineViewDelegate, ContextMenuDelegate> {
}
......
......@@ -33,20 +33,24 @@
#import <QSortFilterProxyModel>
#import <callmodel.h>
#import <call.h>
#import <person.h>
#import <contactmethod.h>
#import <localhistorycollection.h>
#import "QNSTreeController.h"
#import "PersonLinkerVC.h"
#define COLUMNID_DAY @"DayColumn" // the single column name in our outline view
#define COLUMNID_CONTACTMETHOD @"ContactMethodColumn" // the single column name in our outline view
#define COLUMNID_DATE @"DateColumn" // the single column name in our outline view
@interface HistoryVC()
@interface HistoryVC() <NSPopoverDelegate, KeyboardShortcutDelegate, ContactLinkedDelegate>
@property QNSTreeController *treeController;
@property (assign) IBOutlet NSOutlineView *historyView;
@property (assign) IBOutlet RingOutlineView *historyView;
@property QSortFilterProxyModel *historyProxyModel;
@property (strong) NSPopover* addToContactPopover;
@end
@implementation HistoryVC
......@@ -58,7 +62,6 @@
{
if (self = [super initWithCoder:aDecoder]) {
NSLog(@"INIT HVC");
}
return self;
}
......@@ -79,6 +82,8 @@
[historyView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
[historyView setTarget:self];
[historyView setDoubleAction:@selector(placeHistoryCall:)];
[historyView setContextMenuDelegate:self];
[historyView setShortcutsDelegate:self];
CategorizedHistoryModel::instance()->addCollection<LocalHistoryCollection>(LoadOptions::FORCE_ENABLED);
}
......@@ -87,6 +92,9 @@
{
if([[treeController selectedNodes] count] > 0) {
QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
if (!qIdx.parent().isValid()) {
return;
}
QVariant var = historyProxyModel->data(qIdx, (int)Call::Role::ContactMethod);
ContactMethod* m = qvariant_cast<ContactMethod*>(var);
if(m){
......@@ -174,4 +182,98 @@
//NSLog(@"outlineViewSelectionDidChange!!");
}
#pragma mark - ContextMenuDelegate
- (NSMenu*) contextualMenuForIndex:(NSIndexPath*) path
{
if([[treeController selectedNodes] count] > 0) {
QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
if (qIdx.parent().isValid() && var.isValid()) {
if (auto call = var.value<Call *>()) {
auto contactmethod = call->peerContactMethod();
if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
NSMenu *theMenu = [[NSMenu alloc]
initWithTitle:@""];
[theMenu insertItemWithTitle:@"Add to contact"
action:@selector(addToContact)
keyEquivalent:@"a"
atIndex:0];
return theMenu;
}
}
}
}
return nil;
}
- (void) addToContact
{
ContactMethod* contactmethod = nullptr;
if([[treeController selectedNodes] count] > 0) {
QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
if (qIdx.parent().isValid() && var.isValid()) {
if (auto call = var.value<Call *>()) {
contactmethod = call->peerContactMethod();
}
}
}
if (self.addToContactPopover != nullptr) {
[self.addToContactPopover performClose:self];
self.addToContactPopover = NULL;
} else if (contactmethod) {
auto* editorVC = [[PersonLinkerVC alloc] initWithNibName:@"PersonEditor" bundle:nil];
[editorVC setMethodToLink:contactmethod];
[editorVC setContactLinkedDelegate:self];
self.addToContactPopover = [[NSPopover alloc] init];
[self.addToContactPopover setContentSize:editorVC.view.frame.size];
[self.addToContactPopover setContentViewController:editorVC];
[self.addToContactPopover setAnimates:YES];
[self.addToContactPopover setBehavior:NSPopoverBehaviorTransient];
[self.addToContactPopover setDelegate:self];
[self.addToContactPopover showRelativeToRect:[historyView frameOfOutlineCellAtRow:[historyView selectedRow]] ofView:historyView preferredEdge:NSMaxXEdge];
}
}
#pragma mark - NSPopOverDelegate
- (void)popoverDidClose:(NSNotification *)notification
{
if (self.addToContactPopover != nullptr) {
[self.addToContactPopover performClose:self];
self.addToContactPopover = NULL;
}
}
#pragma mark - ContactLinkedDelegate
- (void)contactLinked
{
if (self.addToContactPopover != nullptr) {
[self.addToContactPopover performClose:self];
self.addToContactPopover = NULL;
}
}
#pragma mark - KeyboardShortcutDelegate
- (void) onAddShortcut
{
if([[treeController selectedNodes] count] > 0) {
QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
if (qIdx.parent().isValid() && var.isValid()) {
if (auto call = var.value<Call *>()) {
auto contactmethod = call->peerContactMethod();
if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
[self addToContact];
}
}
}
}
}
@end
/*
* Copyright (C) 2015 Savoir-faire Linux Inc.
* Author: Alexandre Lision <alexandre.lision@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Additional permission under GNU GPL version 3 section 7:
*
* If you modify this program, or any covered work, by linking or
* combining it with the OpenSSL project's OpenSSL library (or a
* modified version of that library), containing parts covered by the
* terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
* grants you additional permission to convey the resulting work.
* Corresponding Source for a non-source form of such a combination
* shall include the source code for the parts of OpenSSL used as well
* as that of the covered work.
*/
#import <Cocoa/Cocoa.h>
@protocol ContactLinkedDelegate;
@protocol ContactLinkedDelegate
@optional
-(void) contactLinked;
@end
class ContactMethod;
@interface PersonLinkerVC : NSViewController <NSOutlineViewDelegate>
@property ContactMethod* const methodToLink;
/*
* Delegate to inform about completion of the linking process between
* a ContactMethod and a Person.
*/
@property (nonatomic) id <ContactLinkedDelegate> contactLinkedDelegate;
@end
/*
* Copyright (C) 2015 Savoir-faire Linux Inc.
* Author: Alexandre Lision <alexandre.lision@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
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Additional permission under GNU GPL version 3 section 7:
*
* If you modify this program, or any covered work, by linking or
* combining it with the OpenSSL project's OpenSSL library (or a
* modified version of that library), containing parts covered by the
* terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
* grants you additional permission to convey the resulting work.
* Corresponding Source for a non-source form of such a combination
* shall include the source code for the parts of OpenSSL used as well
* as that of the covered work.
*/
#import "PersonLinkerVC.h"
//Qt
#import <QtMacExtras/qmacfunctions.h>
#import <QPixmap>
//LRC
#import <person.h>
#import <personmodel.h>
#import <contactmethod.h>
#import <numbercategorymodel.h>
#import "QNSTreeController.h"
#import "delegates/ImageManipulationDelegate.h"
#import "views/PersonCell.h"
#define FIRSTNAME_TAG 1
#define LASTNAME_TAG 2
#define COLUMNID_NAME @"NameColumn"
class OnlyPersonProxyModel : public QSortFilterProxyModel
{
public:
OnlyPersonProxyModel(QAbstractItemModel* parent) : QSortFilterProxyModel(parent)
{
setSourceModel(parent);
}
virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
bool match = filterRegExp().indexIn(sourceModel()->index(source_row,0,source_parent).data(Qt::DisplayRole).toString()) != -1;
//qDebug() << "FILTERING" << sourceModel()->index(source_row,0,source_parent) << "match:" << match;
return match && !sourceModel()->index(source_row,0,source_parent).parent().isValid();
}
};
@interface PersonLinkerVC () <NSTextFieldDelegate, NSComboBoxDelegate, NSComboBoxDataSource>
@property QSortFilterProxyModel* contactProxyModel;
@property QNSTreeController* treeController;
@property (unsafe_unretained) IBOutlet NSTextField *contactMethodLabel;
@property (unsafe_unretained) IBOutlet NSOutlineView *personsView;
@property (unsafe_unretained) IBOutlet NSTextField *firstNameField;
@property (unsafe_unretained) IBOutlet NSTextField *lastNameField;
@property (unsafe_unretained) IBOutlet NSButton *createNewContactButton;
@property (unsafe_unretained) IBOutlet NSComboBox *categoryComboBox;
@property (strong) IBOutlet NSView *createContactSubview;
@property (unsafe_unretained) IBOutlet NSView *linkToExistingSubview;
@end
@implementation PersonLinkerVC
@synthesize treeController;
@synthesize personsView;
@synthesize contactProxyModel;
@synthesize contactMethodLabel;
@synthesize categoryComboBox, firstNameField, lastNameField;
@synthesize createContactSubview, linkToExistingSubview, createNewContactButton;
-(void) awakeFromNib
{
NSLog(@"INIT PersonLinkerVC");
[firstNameField setTag:FIRSTNAME_TAG];
[lastNameField setTag:LASTNAME_TAG];
[categoryComboBox selectItemAtIndex:0];
contactProxyModel = new OnlyPersonProxyModel(PersonModel::instance());
contactProxyModel->setSortRole(static_cast<int>(Qt::DisplayRole));
contactProxyModel->sort(0,Qt::AscendingOrder);
contactProxyModel->setFilterRole(Qt::DisplayRole);
treeController = [[QNSTreeController alloc] initWithQModel:contactProxyModel];
[treeController setAvoidsEmptySelection:NO];
[treeController setChildrenKeyPath:@"children"];
[personsView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
[personsView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
[personsView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
[personsView setTarget:self];
[personsView setDoubleAction:@selector(addToContact:)];
[contactMethodLabel setStringValue:self.methodToLink->uri().toNSString()];
}
- (IBAction)addToContact:(id)sender
{
/* get the selected number category */
const auto& idx = NumberCategoryModel::instance()->index([categoryComboBox indexOfSelectedItem]);
if (idx.isValid()) {
auto category = NumberCategoryModel::instance()->getCategory(idx.data().toString());
self.methodToLink->setCategory(category);
}
if([[treeController selectedNodes] count] > 0) {
QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
ContactMethod* m = nil;
if(((NSTreeNode*)[treeController selectedNodes][0]).indexPath.length == 1) {
// Person
QVariant var = qIdx.data((int)Person::Role::Object);
if (var.isValid()) {
Person *p = var.value<Person*>();
Person::ContactMethods cms = p->phoneNumbers();
cms.append(self.methodToLink);
p->setContactMethods(cms);
self.methodToLink->setPerson(p);
p->save();
[self.contactLinkedDelegate contactLinked];
}
}
}
}
- (void) dealloc
{
// No ARC for c++ pointers
delete contactProxyModel;
}
- (IBAction)presentNewContactForm:(id)sender {
[createContactSubview setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
//[createContactSubview setBounds:linkToExistingSubview.bounds];
[createContactSubview setFrame:linkToExistingSubview.frame];
[linkToExistingSubview setHidden:YES];
[self.view addSubview:createContactSubview];
[[[NSApplication sharedApplication] mainWindow] makeFirstResponder:firstNameField];
[firstNameField setNextKeyView:lastNameField];
[lastNameField setNextKeyView:createNewContactButton];
[createNewContactButton setNextKeyView:firstNameField];
}
- (IBAction)createContact:(id)sender
{
/* get the selected number category */
const auto& idx = NumberCategoryModel::instance()->index([categoryComboBox indexOfSelectedItem]);
if (idx.isValid()) {
auto category = NumberCategoryModel::instance()->getCategory(idx.data().toString());
self.methodToLink->setCategory(category);
}
/* create a new person */
Person *p = new Person();
p->setFirstName(QString::fromNSString(firstNameField.stringValue));
p->setFamilyName(QString::fromNSString(lastNameField.stringValue));
p->setFormattedName(QString::fromNSString([[NSString alloc] initWithFormat:@"%@ %@", firstNameField.stringValue, lastNameField.stringValue]));
/* associate the new person with the contact method */
Person::ContactMethods numbers;
numbers << self.methodToLink;
p->setContactMethods(numbers);
self.methodToLink->setPerson(p);
PersonModel::instance()->addNewPerson(p);
[self.contactLinkedDelegate contactLinked];
}
#pragma mark - NSOutlineViewDelegate methods
// -------------------------------------------------------------------------------
// shouldSelectItem:item
// -------------------------------------------------------------------------------
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
if(!qIdx.isValid())
return NO;
if(qIdx.parent().isValid()) {
return NO;
} else {
return YES;
}
}
// -------------------------------------------------------------------------------
// dataCellForTableColumn:tableColumn:item
// -------------------------------------------------------------------------------
- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
PersonCell *returnCell = [tableColumn dataCell];
return returnCell;
}
// -------------------------------------------------------------------------------
// shouldEditTableColumn:tableColumn:item
//
// Decide to allow the edit of the given outline view "item".
// -------------------------------------------------------------------------------
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
return NO;
}
// -------------------------------------------------------------------------------
// outlineView:willDisplayCell:forTableColumn:item
// -------------------------------------------------------------------------------
- (void)outlineView:(NSOutlineView *)olv willDisplayCell:(NSCell*)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
if(!qIdx.isValid()) {
[((PersonCell *)cell) setPersonImage:nil];
return;
}
if ([[tableColumn identifier] isEqualToString:COLUMNID_NAME])
{
PersonCell *pCell = (PersonCell *)cell;
[pCell setPersonImage:nil];
if(!qIdx.parent().isValid()) {
pCell.title = qIdx.data(Qt::DisplayRole).toString().toNSString();
Person* p = qvariant_cast<Person*>(qIdx.data((int)Person::Role::Object));
QVariant photo = ImageManipulationDelegate::instance()->contactPhoto(p, QSize(35,35));
[pCell setPersonImage:QtMac::toNSImage(qvariant_cast<QPixmap>(photo))];
} else {
pCell.title = qIdx.data(Qt::DisplayRole).toString().toNSString();
}
}
}
// -------------------------------------------------------------------------------
// outlineViewSelectionDidChange:notification
// -------------------------------------------------------------------------------
- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
{
return 45.0;
}
#pragma mark - NSTextFieldDelegate
- (void)controlTextDidChange:(NSNotification *) notification
{
if ([notification.object tag] == FIRSTNAME_TAG || [notification.object tag] == LASTNAME_TAG) {