From c73d37b50e1a4130b39f1978f85995ce0f209eed Mon Sep 17 00:00:00 2001
From: Alexandre Lision <alexandre.lision@gmail.com>
Date: Sun, 15 Mar 2015 18:43:07 -0400
Subject: [PATCH] contacts: display reachable contacts

This commits introduces integration with user's contacts
Only reachable contacts are displayed, based on accounts available.
No edition is provided within Ring.

(cherry picked from commit 3b0bd33ca7082eedfa680cdc5be91d3d4e7ec211)

Change-Id: I6ef76ff5547abc9875f14f90027ff97336889e75
---
 CMakeLists.txt                             |  13 ++
 src/HistoryViewController.mm               |   5 +-
 src/PersonsVC.h                            |  35 +++
 src/PersonsVC.mm                           | 241 ++++++++++++++++++++
 src/QNSTreeController.mm                   |  32 ++-
 src/backends/AddressBookBackend.h          |  62 +++++
 src/backends/AddressBookBackend.mm         | 251 +++++++++++++++++++++
 src/delegates/ImageManipulationDelegate.h  |  57 +++++
 src/delegates/ImageManipulationDelegate.mm | 163 +++++++++++++
 src/views/PersonCell.h                     |  43 ++++
 src/views/PersonCell.mm                    | 182 +++++++++++++++
 ui/RingWindow.xib                          | 106 +++++++--
 12 files changed, 1160 insertions(+), 30 deletions(-)
 create mode 100644 src/PersonsVC.h
 create mode 100644 src/PersonsVC.mm
 create mode 100644 src/backends/AddressBookBackend.h
 create mode 100644 src/backends/AddressBookBackend.mm
 create mode 100644 src/delegates/ImageManipulationDelegate.h
 create mode 100644 src/delegates/ImageManipulationDelegate.mm
 create mode 100644 src/views/PersonCell.h
 create mode 100644 src/views/PersonCell.mm

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 85816bb1..48c04785 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,10 +16,12 @@ ADD_DEFINITIONS("-std=c++11")
 PROJECT(${PROJ_NAME})
 
 FIND_PACKAGE(Qt5Core REQUIRED)
+FIND_PACKAGE(Qt5MacExtras REQUIRED)
 FIND_PACKAGE(Qt5Widgets REQUIRED)
 FIND_PACKAGE(LibRingClient REQUIRED)
 
 INCLUDE_DIRECTORIES(SYSTEM ${Qt5Core_INCLUDE_DIRS})
+INCLUDE_DIRECTORIES(SYSTEM ${Qt5MacExtras_INCLUDE_DIRS})
 INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR})
 INCLUDE_DIRECTORIES( ${CMAKE_CURRENT_SOURCE_DIR})
 INCLUDE_DIRECTORIES(${LIB_RING_CLIENT_INCLUDE_DIR})
@@ -48,9 +50,14 @@ SET(ringclient_SRCS
    src/GeneralPrefsVC.mm
    src/RingWizardWC.mm
    src/HistoryViewController.mm
+   src/PersonsVC.mm
 
+   src/delegates/ImageManipulationDelegate.mm
+
+   src/views/PersonCell.mm
    src/views/CallView.mm
 
+   src/backends/AddressBookBackend.mm
    src/backends/MinimalHistoryBackend.mm)
 
 SET(ringclient_XIBS
@@ -89,9 +96,14 @@ SET(ringclient_HDRS
    src/HistoryViewController.h
    src/RingWizardWC.h
    src/QNSTreeController.h
+   src/PersonsVC.h
+
+   src/delegates/ImageManipulationDelegate.h
 
+   src/views/PersonCell.h
    src/views/CallView.h
 
+   src/backends/AddressBookBackend.h
    src/backends/MinimalHistoryBackend.h)
 
 # Icons
@@ -134,6 +146,7 @@ ADD_EXECUTABLE(${PROJ_NAME} MACOSX_BUNDLE
 TARGET_LINK_LIBRARIES( ${PROJ_NAME}
    ${LIB_RING_CLIENT_LIBRARY}
    ${Qt5Core_LIBRARIES}
+   ${Qt5MacExtras_LIBRARIES}
    ${Qt5Widgets_LIBRARIES}
 )
 
diff --git a/src/HistoryViewController.mm b/src/HistoryViewController.mm
index 5ca5dada..60d05b0e 100644
--- a/src/HistoryViewController.mm
+++ b/src/HistoryViewController.mm
@@ -78,15 +78,14 @@
     [historyView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
     [historyView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
     [historyView setTarget:self];
-    [historyView setDoubleAction:@selector(placeCall:)];
+    [historyView setDoubleAction:@selector(placeHistoryCall:)];
 
     CategorizedHistoryModel::instance()->addCollection<MinimalHistoryBackend>(LoadOptions::FORCE_ENABLED);
 }
 
-- (void)placeCall:(id)sender
+- (void)placeHistoryCall:(id)sender
 {
     if([[treeController selectedNodes] count] > 0) {
-        Call* c = CallModel::instance()->dialingCall();
         QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
         QVariant var = historyProxyModel->data(qIdx, (int)Call::Role::ContactMethod);
         ContactMethod* m = qvariant_cast<ContactMethod*>(var);
diff --git a/src/PersonsVC.h b/src/PersonsVC.h
new file mode 100644
index 00000000..9d735717
--- /dev/null
+++ b/src/PersonsVC.h
@@ -0,0 +1,35 @@
+/*
+ *  Copyright (C) 2004-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>
+
+@interface PersonsVC : NSViewController <NSOutlineViewDelegate>
+
+@end
diff --git a/src/PersonsVC.mm b/src/PersonsVC.mm
new file mode 100644
index 00000000..bf9e6b40
--- /dev/null
+++ b/src/PersonsVC.mm
@@ -0,0 +1,241 @@
+/*
+ *  Copyright (C) 2004-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 "PersonsVC.h"
+
+#import <personmodel.h>
+#import <callmodel.h>
+#import <categorizedcontactmodel.h>
+#import <QSortFilterProxyModel>
+#import <person.h>
+#import <contactmethod.h>
+#import <QtMacExtras/qmacfunctions.h>
+#import <QPixmap>
+
+#import "backends/AddressBookBackend.h"
+#import "QNSTreeController.h"
+#import "delegates/ImageManipulationDelegate.h"
+#import "views/PersonCell.h"
+
+#define COLUMNID_NAME @"NameColumn"
+
+class ReachablePersonModel : public QSortFilterProxyModel
+{
+public:
+    ReachablePersonModel(QAbstractItemModel* parent) : QSortFilterProxyModel(parent)
+    {
+        setSourceModel(parent);
+    }
+    virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
+    {
+        return sourceModel()->index(source_row,0,source_parent).flags() & Qt::ItemIsEnabled;
+    }
+};
+
+
+@interface PersonsVC ()
+
+@property NSTreeController *treeController;
+@property (assign) IBOutlet NSOutlineView *personsView;
+@property QSortFilterProxyModel *contactProxyModel;
+
+@end
+
+@implementation PersonsVC
+@synthesize treeController;
+@synthesize personsView;
+@synthesize contactProxyModel;
+
+-(void) awakeFromNib
+{
+    new ImageManipulationDelegate();
+    NSLog(@"INIT PersonsVC");
+    contactProxyModel = new ReachablePersonModel(CategorizedContactModel::instance());
+    contactProxyModel->setSortRole(static_cast<int>(Qt::DisplayRole));
+    contactProxyModel->sort(0,Qt::AscendingOrder);
+    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(callContact:)];
+
+    CategorizedContactModel::instance()->setUnreachableHidden(YES);
+    PersonModel::instance()->addCollection<AddressBookBackend>(LoadOptions::FORCE_ENABLED);
+
+}
+
+- (IBAction)callContact:(id)sender
+{
+    if([[treeController selectedNodes] count] > 0) {
+        QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
+        ContactMethod* m = nil;
+        if(((NSTreeNode*)[treeController selectedNodes][0]).indexPath.length == 2) {
+            // Person
+            QVariant var = qIdx.data((int)Person::Role::Object);
+            if (var.isValid()) {
+                Person *c = var.value<Person*>();
+                if (c->phoneNumbers().size() == 1) {
+                    m = c->phoneNumbers().first();
+                }
+            }
+        } else if (((NSTreeNode*)[treeController selectedNodes][0]).indexPath.length == 3) {
+            //ContactMethod
+            QVariant var = qIdx.data(static_cast<int>(ContactMethod::Role::Object));
+            if (var.isValid()) {
+                m = var.value<ContactMethod *>();
+            }
+        }
+
+        if(m){
+            Call* c = CallModel::instance()->dialingCall();
+            c->setDialNumber(m);
+            c << Call::Action::ACCEPT;
+        }
+    }
+}
+
+#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];
+    if(!qIdx.isValid())
+        return returnCell;
+
+    if(!qIdx.parent().isValid()) {
+        [returnCell setDrawsBackground:YES];
+        [returnCell setBackgroundColor:[NSColor selectedControlColor]];
+    } else {
+        [returnCell setDrawsBackground:NO];
+    }
+
+    return returnCell;
+}
+
+// -------------------------------------------------------------------------------
+//	textShouldEndEditing:fieldEditor
+// -------------------------------------------------------------------------------
+- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor
+{
+    if ([[fieldEditor string] length] == 0)
+    {
+        // don't allow empty node names
+        return NO;
+    }
+    else
+    {
+        return YES;
+    }
+}
+
+// -------------------------------------------------------------------------------
+//	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())
+        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();
+        } else {
+            pCell.title = qIdx.data(Qt::DisplayRole).toString().toNSString();
+            if(((NSTreeNode*)item).indexPath.length == 2) {
+                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))];
+            }
+        }
+    }
+}
+
+// -------------------------------------------------------------------------------
+//	outlineViewSelectionDidChange:notification
+// -------------------------------------------------------------------------------
+- (void)outlineViewSelectionDidChange:(NSNotification *)notification
+{
+
+}
+
+- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
+{
+    QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
+    if(!qIdx.isValid())
+        return 0.0f;
+
+    if(!qIdx.parent().isValid()) {
+        return 20.0;
+    } else {
+        return 45.0;
+    }
+}
+
+
+@end
diff --git a/src/QNSTreeController.mm b/src/QNSTreeController.mm
index 75533c25..eca48848 100644
--- a/src/QNSTreeController.mm
+++ b/src/QNSTreeController.mm
@@ -84,13 +84,33 @@
     NSUInteger myArray[[idx length]];
     [idx getIndexes:myArray];
     QModelIndex toReturn;
-    if(idx.length == 2)
-        toReturn = self->privateQModel->index(myArray[1], 0, self->privateQModel->index(myArray[0], 0));
-    else
-        toReturn = self->privateQModel->index(myArray[0], 0);
+
+    for (int i = 0; i < idx.length; ++i) {
+        toReturn = self->privateQModel->index(myArray[i], 0, toReturn);
+    }
+
     return toReturn;
 }
 
+- (void) insertChildAtQIndex:(QModelIndex) qIdx
+{
+    Node* child = [[Node alloc] init];
+
+    QModelIndex tmp = qIdx.parent();
+    NSMutableArray* allIndexes = [NSMutableArray array];
+    while (tmp.isValid()) {
+        [allIndexes insertObject:@(tmp.row()) atIndex:0];
+        tmp = tmp.parent();
+    }
+    [allIndexes insertObject:@(qIdx.row()) atIndex:allIndexes.count];
+
+    NSUInteger indexes[allIndexes.count];
+    for (int i = 0 ; i < allIndexes.count ; ++i) {
+        indexes[i] = [[allIndexes objectAtIndex:i] intValue];
+    }
+    [self insertObject:child atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndexes:indexes length:allIndexes.count]];
+}
+
 - (void)connect
 {
     QObject::connect(self->privateQModel,
@@ -102,9 +122,7 @@
                                  Node* n = [[Node alloc] init];
                                  [self insertObject:n atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndex:row]];
                              } else {
-                                 Node* child = [[Node alloc] init];
-                                 NSUInteger indexes[] = { (NSUInteger)parent.row(), (NSUInteger)row};
-                                 [self insertObject:child atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndexes:indexes length:2]];
+                                 [self insertChildAtQIndex:self->privateQModel->index(row, 0, parent)];
                              }
                          }
                      }
diff --git a/src/backends/AddressBookBackend.h b/src/backends/AddressBookBackend.h
new file mode 100644
index 00000000..78568519
--- /dev/null
+++ b/src/backends/AddressBookBackend.h
@@ -0,0 +1,62 @@
+/*
+ *  Copyright (C) 2004-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.
+ */
+#ifndef ADDRESSBOOKBACKEND_H
+#define ADDRESSBOOKBACKEND_H
+
+#include <collectioninterface.h>
+#include <collectioneditor.h>
+
+class Person;
+
+template<typename T> class CollectionMediator;
+
+class AddressBookBackend : public CollectionInterface
+{
+public:
+    explicit AddressBookBackend(CollectionMediator<Person>* mediator);
+    virtual ~AddressBookBackend();
+
+    virtual bool load() override;
+    virtual bool reload() override;
+    virtual bool clear() override;
+    virtual QString    name     () const override;
+    virtual QString    category () const override;
+    virtual QVariant   icon     () const override;
+    virtual bool       isEnabled() const override;
+    virtual QByteArray id       () const override;
+    virtual SupportedFeatures  supportedFeatures() const override;
+
+private:
+    CollectionMediator<Person>*  m_pMediator;
+
+    void asyncLoad(int startingPoint);
+};
+
+#endif // ADDRESSBOOKBACKEND_H
diff --git a/src/backends/AddressBookBackend.mm b/src/backends/AddressBookBackend.mm
new file mode 100644
index 00000000..d2073c00
--- /dev/null
+++ b/src/backends/AddressBookBackend.mm
@@ -0,0 +1,251 @@
+/*
+ *  Copyright (C) 2004-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 import the source code for the parts of OpenSSL used as well
+ *  as that of the covered work.
+ */
+#import "AddressBookBackend.h"
+
+#import <AddressBook/AddressBook.h>
+
+//Qt
+#import <QtCore/QFile>
+#import <QtCore/QDir>
+#import <QtCore/QHash>
+#import <QtWidgets/QApplication>
+#import <QtCore/QStandardPaths>
+#import <QTimer>
+#import <QtGlobal>
+
+//Ring
+#import <Person.h>
+#import <account.h>
+#import <person.h>
+#import <contactmethod.h>
+
+/**
+ *
+ *kABFirstNameProperty
+ kABLastNameProperty
+ kABFirstNamePhoneticProperty
+ kABLastNamePhoneticProperty
+ kABBirthdayProperty
+ kABOrganizationProperty
+ kABJobTitleProperty
+ kABHomePageProperty
+ kABEmailProperty
+ kABAddressProperty
+ kABPhoneProperty
+ kABAIMInstantProperty
+ kABJabberInstantProperty
+ kABMSNInstantProperty
+ kABYahooInstantProperty
+ kABICQInstantProperty
+ kABNoteProperty
+ kABMiddleNameProperty
+ kABMiddleNamePhoneticProperty
+ kABTitleProperty
+ kABSuffixProperty
+ kABNicknameProperty
+ kABMaidenNameProperty
+ */
+
+class AddressBookEditor : public CollectionEditor<Person>
+{
+public:
+    AddressBookEditor(CollectionMediator<Person>* m, AddressBookBackend* parent);
+    virtual bool save       ( const Person* item ) override;
+    virtual bool remove     ( const Person* item ) override;
+    virtual bool edit       ( Person*       item ) override;
+    virtual bool addNew     ( const Person* item ) override;
+    virtual bool addExisting( const Person* item ) override;
+
+private:
+    virtual QVector<Person*> items() const override;
+
+    //Helpers
+    void savePerson(QTextStream& stream, const Person* Person);
+    bool regenFile(const Person* toIgnore);
+
+    //Attributes
+    QVector<Person*> m_lItems;
+    AddressBookBackend* m_pCollection;
+};
+
+AddressBookEditor::AddressBookEditor(CollectionMediator<Person>* m, AddressBookBackend* parent) :
+CollectionEditor<Person>(m),m_pCollection(parent)
+{
+
+}
+
+AddressBookBackend::AddressBookBackend(CollectionMediator<Person>* mediator) :
+CollectionInterface(new AddressBookEditor(mediator,this)),m_pMediator(mediator)
+{
+
+}
+
+AddressBookBackend::~AddressBookBackend()
+{
+
+}
+
+void AddressBookEditor::savePerson(QTextStream& stream, const Person* Person)
+{
+
+    qDebug() << "Saving Person!";
+}
+
+bool AddressBookEditor::regenFile(const Person* toIgnore)
+{
+    QDir dir(QString('/'));
+    dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QString());
+
+
+    return false;
+}
+
+bool AddressBookEditor::save(const Person* Person)
+{
+    //if (Person->collection()->editor<Person>() != this)
+    //    return addNew(Person);
+
+    return regenFile(nullptr);
+}
+
+bool AddressBookEditor::remove(const Person* item)
+{
+    return regenFile(item);
+}
+
+bool AddressBookEditor::edit( Person* item)
+{
+    Q_UNUSED(item)
+    return false;
+}
+
+bool AddressBookEditor::addNew(const Person* Person)
+{
+    QDir dir(QString('/'));
+    dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QString());
+
+    return false;
+}
+
+bool AddressBookEditor::addExisting(const Person* item)
+{
+    m_lItems << const_cast<Person*>(item);
+    mediator()->addItem(item);
+    return true;
+}
+
+QVector<Person*> AddressBookEditor::items() const
+{
+    return m_lItems;
+}
+
+QString AddressBookBackend::name () const
+{
+    return QObject::tr("AddressBook backend");
+}
+
+QString AddressBookBackend::category () const
+{
+    return QObject::tr("Persons");
+}
+
+QVariant AddressBookBackend::icon() const
+{
+    return QVariant();
+}
+
+bool AddressBookBackend::isEnabled() const
+{
+    return true;
+}
+
+bool AddressBookBackend::load()
+{
+    QTimer::singleShot(100, [=] {
+        asyncLoad(0);
+    });
+     return false;
+}
+
+void AddressBookBackend::asyncLoad(int startingPoint)
+{
+    ABAddressBook *book = [ABAddressBook sharedAddressBook];
+    NSArray *everyone = [book people];
+    int endPoint = qMin(startingPoint + 10, (int)everyone.count);
+
+    for (int i = startingPoint; i < endPoint; ++i) {
+
+        Person* person = new Person(QByteArray::fromNSData(((ABPerson*)[everyone objectAtIndex:i]).vCardRepresentation),
+                                    Person::Encoding::vCard,
+                                    this);
+        if([person->formattedName().toNSString() isEqualToString:@""]   &&
+           [person->secondName().toNSString() isEqualToString:@""]     &&
+           [person->firstName().toNSString() isEqualToString:@""]) {
+            continue;
+        }
+        person->setCollection(this);
+
+        editor<Person>()->addExisting(person);
+    }
+
+    if(endPoint < everyone.count) {
+        QTimer::singleShot(100, [=] {
+            asyncLoad(endPoint);
+        });
+    }
+
+}
+
+
+bool AddressBookBackend::reload()
+{
+    return false;
+}
+
+CollectionInterface::SupportedFeatures AddressBookBackend::supportedFeatures() const
+{
+    return (CollectionInterface::SupportedFeatures) (
+                                                     CollectionInterface::SupportedFeatures::NONE  |
+                                                     CollectionInterface::SupportedFeatures::LOAD  |
+                                                     CollectionInterface::SupportedFeatures::CLEAR |
+                                                     CollectionInterface::SupportedFeatures::REMOVE|
+                                                     CollectionInterface::SupportedFeatures::ADD   );
+}
+
+bool AddressBookBackend::clear()
+{
+    /* TODO: insert confirm dialog? */
+    return true;
+}
+
+QByteArray AddressBookBackend::id() const
+{
+    return "abb";
+}
diff --git a/src/delegates/ImageManipulationDelegate.h b/src/delegates/ImageManipulationDelegate.h
new file mode 100644
index 00000000..2f711aea
--- /dev/null
+++ b/src/delegates/ImageManipulationDelegate.h
@@ -0,0 +1,57 @@
+/*
+ *  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.
+ */
+#ifndef IMAGEMANIPULATION_H
+#define IMAGEMANIPULATION_H
+
+#import <QuartzCore/QuartzCore.h>
+
+#import <delegates/pixmapmanipulationdelegate.h>
+#import <call.h>
+
+class Person;
+class QPixmap;
+
+class ImageManipulationDelegate : public PixmapManipulationDelegate {
+
+public:
+    ImageManipulationDelegate();
+    QVariant contactPhoto(Person* c, const QSize& size, bool displayPresence = true) override;
+    virtual QByteArray toByteArray(const QVariant& pxm) override;
+    virtual QVariant personPhoto(const QByteArray& data, const QString& type = "PNG") override;
+
+private:
+    //Helper
+    QPixmap drawDefaultUserPixmap(const QSize& size, bool displayPresence, bool isPresent);
+    CGImageRef resizeCGImage(CGImageRef image, const QSize& size);
+
+
+};
+
+#endif // IMAGEMANIPULATION_H
diff --git a/src/delegates/ImageManipulationDelegate.mm b/src/delegates/ImageManipulationDelegate.mm
new file mode 100644
index 00000000..658785fe
--- /dev/null
+++ b/src/delegates/ImageManipulationDelegate.mm
@@ -0,0 +1,163 @@
+/*
+ *  Copyright (C) 2004-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 import the source code for the parts of OpenSSL used as well
+ *  as that of the covered work.
+ */
+#import "ImageManipulationDelegate.h"
+
+#import <Cocoa/Cocoa.h>
+#import <Foundation/Foundation.h>
+
+//Qt
+#import <QSize>
+#import <QBuffer>
+#import <QtGui/QColor>
+#import <QtGui/QPainter>
+#import <QtGui/QBitmap>
+#import <QtWidgets/QApplication>
+#import <QtGui/QImage>
+#import <QtMacExtras/qmacfunctions.h>
+#import <QtGui/QPalette>
+
+//Ring
+#import <person.h>
+#import <contactmethod.h>
+#import <presencestatusmodel.h>
+#import <securityvalidationmodel.h>
+#import <collectioninterface.h>
+#import <useractionmodel.h>
+#import <QStandardPaths>
+
+ImageManipulationDelegate::ImageManipulationDelegate() : PixmapManipulationDelegate()
+{
+
+}
+
+QVariant ImageManipulationDelegate::contactPhoto(Person* c, const QSize& size, bool displayPresence) {
+    const int radius = (size.height() > 35) ? 7 : 5;
+
+    QPixmap pxm;
+    if (c->photo().isValid()) {
+        QPixmap contactPhoto((qvariant_cast<QPixmap>(c->photo())).scaledToWidth(size.height()-6));
+        pxm = QPixmap(size);
+        pxm.fill(Qt::transparent);
+        QPainter painter(&pxm);
+
+        //Clear the pixmap
+        painter.setCompositionMode(QPainter::CompositionMode_Clear);
+        painter.fillRect(0,0,size.width(),size.height(),QBrush(Qt::white));
+        painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
+
+        //Add corner radius to the Pixmap
+        QRect pxRect = contactPhoto.rect();
+        QBitmap mask(pxRect.size());
+        QPainter customPainter(&mask);
+        customPainter.setRenderHint  (QPainter::Antialiasing, true      );
+        customPainter.fillRect       (pxRect                , Qt::white );
+        customPainter.setBackground  (Qt::black                         );
+        customPainter.setBrush       (Qt::black                         );
+        customPainter.drawRoundedRect(pxRect,radius,radius);
+        contactPhoto.setMask(mask);
+        painter.drawPixmap(3,3,contactPhoto);
+        painter.setBrush(Qt::NoBrush);
+        painter.setPen(Qt::white);
+        painter.setRenderHint  (QPainter::Antialiasing, true   );
+        painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+        painter.drawRoundedRect(3,3,pxm.height()-6,pxm.height()-6,radius,radius);
+        painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
+
+    }
+    else {
+        pxm = drawDefaultUserPixmap(size, false, false);
+    }
+    
+    return pxm;
+}
+
+QVariant ImageManipulationDelegate::personPhoto(const QByteArray& data, const QString& type)
+{
+    QImage image;
+    //For now, ENCODING is only base64 and image type PNG or JPG
+    const bool ret = image.loadFromData(QByteArray::fromBase64(data),type.toLatin1());
+    if (!ret)
+        qDebug() << "vCard image loading failed";
+
+    return QPixmap::fromImage(image);
+}
+
+QByteArray ImageManipulationDelegate::toByteArray(const QVariant& pxm)
+{
+    //Preparation of our QPixmap
+    QByteArray bArray;
+    QBuffer buffer(&bArray);
+    buffer.open(QIODevice::WriteOnly);
+
+    //PNG ?
+    (qvariant_cast<QPixmap>(pxm)).save(&buffer, "PNG");
+    buffer.close();
+
+    return bArray;
+}
+
+QPixmap ImageManipulationDelegate::drawDefaultUserPixmap(const QSize& size, bool displayPresence, bool isPresent) {
+
+    QPixmap pxm(size);
+    pxm.fill(Qt::transparent);
+    QPainter painter(&pxm);
+
+    // create the image somehow, load from file, draw into it...
+    CGImageSourceRef source;
+
+    source = CGImageSourceCreateWithData((CFDataRef)[[NSImage imageNamed:@"NSUser"] TIFFRepresentation], NULL);
+    CGImageRef maskRef =  CGImageSourceCreateImageAtIndex(source, 0, NULL);
+    painter.drawPixmap(3,3,QtMac::fromCGImageRef(resizeCGImage(maskRef, size)));
+
+    return pxm;
+}
+
+CGImageRef ImageManipulationDelegate::resizeCGImage(CGImageRef image, const QSize& size) {
+    // create context, keeping original image properties
+    CGColorSpaceRef colorspace = CGImageGetColorSpace(image);
+    CGContextRef context = CGBitmapContextCreate(NULL, size.width(), size.height(),
+                                                 CGImageGetBitsPerComponent(image),
+                                                 CGImageGetBytesPerRow(image),
+                                                 colorspace,
+                                                 CGImageGetAlphaInfo(image));
+    CGColorSpaceRelease(colorspace);
+
+
+    if(context == NULL)
+        return nil;
+
+    // draw image to context (resizing it)
+    CGContextDrawImage(context, CGRectMake(0, 0, size.width(), size.height()), image);
+    // extract resulting image from context
+    CGImageRef imgRef = CGBitmapContextCreateImage(context);
+    CGContextRelease(context);
+
+    return imgRef;
+}
\ No newline at end of file
diff --git a/src/views/PersonCell.h b/src/views/PersonCell.h
new file mode 100644
index 00000000..34aeecdb
--- /dev/null
+++ b/src/views/PersonCell.h
@@ -0,0 +1,43 @@
+/*
+ *  Copyright (C) 2004-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>
+
+
+@interface PersonCell : NSTextFieldCell
+{
+	BOOL mIsEditingOrSelecting;
+
+}
+@property NSImage *personImage;
+@property NSString* primaryText;
+@property NSString* secondaryText;
+
+@end
diff --git a/src/views/PersonCell.mm b/src/views/PersonCell.mm
new file mode 100644
index 00000000..9b6bede5
--- /dev/null
+++ b/src/views/PersonCell.mm
@@ -0,0 +1,182 @@
+/*
+ *  Copyright (C) 2004-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 "PersonCell.h"
+
+#define kImageOriginXOffset     3
+#define kImageOriginYOffset     1
+
+#define kTextOriginXOffset      2
+#define kTextOriginYOffset      2
+#define kTextHeightAdjust       4
+
+@implementation PersonCell
+
+// -------------------------------------------------------------------------------
+//	initTextCell:aString
+// -------------------------------------------------------------------------------
+- (instancetype)initTextCell:(NSString *)aString
+{
+    self = [super initTextCell:aString];
+    if (self != nil)
+    {
+        // we want a smaller font
+        [self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+    }
+    return self;
+}
+
+// -------------------------------------------------------------------------------
+//	copyWithZone:zone
+// -------------------------------------------------------------------------------
+- (id)copyWithZone:(NSZone *)zone
+{
+    PersonCell *cell = (PersonCell *)[super copyWithZone:zone];
+    cell.personImage = self.personImage;
+    return cell;
+}
+
+// -------------------------------------------------------------------------------
+//	titleRectForBounds:cellRect
+//
+//	Returns the proper bound for the cell's title while being edited
+// -------------------------------------------------------------------------------
+- (NSRect)titleRectForBounds:(NSRect)cellRect
+{
+    // the cell has an image: draw the normal item cell
+    NSSize imageSize;
+    NSRect imageFrame;
+
+    imageSize = [self.personImage size];
+    NSDivideRect(cellRect, &imageFrame, &cellRect, 3 + imageSize.width, NSMinXEdge);
+
+    imageFrame.origin.x += kImageOriginXOffset;
+    imageFrame.origin.y -= kImageOriginYOffset;
+    imageFrame.size = imageSize;
+
+    imageFrame.origin.y += ceil((cellRect.size.height - imageFrame.size.height) / 2);
+
+    NSRect newFrame = cellRect;
+    newFrame.origin.x += kTextOriginXOffset;
+    newFrame.origin.y += kTextOriginYOffset;
+    newFrame.size.height -= kTextHeightAdjust;
+
+    return newFrame;
+}
+
+// -------------------------------------------------------------------------------
+//	editWithFrame:inView:editor:delegate:event
+// -------------------------------------------------------------------------------
+- (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent
+{
+    NSRect textFrame = [self titleRectForBounds:aRect];
+    [super editWithFrame:textFrame inView:controlView editor:textObj delegate:anObject event:theEvent];
+}
+
+// -------------------------------------------------------------------------------
+//	selectWithFrame:inView:editor:delegate:event:start:length
+// -------------------------------------------------------------------------------
+- (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(NSInteger)selStart length:(NSInteger)selLength
+{
+    NSRect textFrame = [self titleRectForBounds:aRect];
+    [super selectWithFrame:textFrame inView:controlView editor:textObj delegate:anObject start:selStart length:selLength];
+}
+
+// -------------------------------------------------------------------------------
+//	drawWithFrame:cellFrame:controlView
+// -------------------------------------------------------------------------------
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
+{
+    NSRect newCellFrame = cellFrame;
+
+    if (self.personImage != nil)
+    {
+        NSSize imageSize;
+        NSRect imageFrame;
+
+        imageSize = [self.personImage size];
+        NSDivideRect(newCellFrame, &imageFrame, &newCellFrame, imageSize.width, NSMinXEdge);
+        if ([self drawsBackground])
+        {
+            [[self backgroundColor] set];
+            NSRectFill(imageFrame);
+        }
+
+        imageFrame.origin.y += 2;
+        imageFrame.size = imageSize;
+
+        [self.personImage drawInRect:imageFrame
+                        fromRect:NSZeroRect
+                       operation:NSCompositeSourceOver
+                        fraction:1.0
+                  respectFlipped:YES
+                           hints:nil];
+    }
+
+    [super drawWithFrame:newCellFrame inView:controlView];
+}
+
+- (NSRect)drawingRectForBounds:(NSRect)theRect
+{
+    // Get the parent's idea of where we should draw
+    NSRect newRect = [super drawingRectForBounds:theRect];
+
+    // When the text field is being
+    // edited or selected, we have to turn off the magic because it screws up
+    // the configuration of the field editor.  We sneak around this by
+    // intercepting selectWithFrame and editWithFrame and sneaking a
+    // reduced, centered rect in at the last minute.
+    if (mIsEditingOrSelecting == NO)
+    {
+        // Get our ideal size for current text
+        NSSize textSize = [self cellSizeForBounds:theRect];
+
+        // Center that in the proposed rect
+        float heightDelta = newRect.size.height - textSize.height;
+        if (heightDelta > 0)
+        {
+            newRect.size.height -= heightDelta;
+            newRect.origin.y += (heightDelta / 2);
+        }
+    }
+    return newRect;
+}
+
+// -------------------------------------------------------------------------------
+//	cellSize
+// -------------------------------------------------------------------------------
+- (NSSize)cellSize
+{
+    NSSize cellSize = [super cellSize];
+    cellSize.width += (self.personImage ? [self.personImage size].width : 0) + 3;
+    return cellSize;
+}
+
+@end
diff --git a/ui/RingWindow.xib b/ui/RingWindow.xib
index df2e1367..17975d94 100644
--- a/ui/RingWindow.xib
+++ b/ui/RingWindow.xib
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6751" systemVersion="14C1510" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6751" systemVersion="13F1066" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
     <dependencies>
         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6751"/>
     </dependencies>
@@ -17,25 +17,25 @@
             <windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="YES"/>
             <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
             <rect key="contentRect" x="196" y="240" width="1053" height="626"/>
-            <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
+            <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1050"/>
             <view key="contentView" autoresizesSubviews="NO" id="se5-gp-TjO">
                 <rect key="frame" x="0.0" y="0.0" width="1053" height="626"/>
                 <autoresizingMask key="autoresizingMask"/>
                 <subviews>
                     <scrollView focusRingType="none" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bqQ-DB-Z0g">
-                        <rect key="frame" x="20" y="461" width="300" height="145"/>
-                        <clipView key="contentView" focusRingType="none" misplaced="YES" id="1so-Pz-QB2">
+                        <rect key="frame" x="20" y="461" width="295" height="145"/>
+                        <clipView key="contentView" focusRingType="none" id="1so-Pz-QB2">
                             <rect key="frame" x="1" y="17" width="238" height="117"/>
                             <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                             <subviews>
                                 <outlineView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" headerView="nuM-bu-a6l" indentationPerLevel="16" outlineTableColumn="VDO-Cu-h8f" id="zcl-pp-rGb">
-                                    <rect key="frame" x="0.0" y="0.0" width="298" height="19"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="293" height="19"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <size key="intercellSpacing" width="3" height="2"/>
                                     <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
                                     <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
                                     <tableColumns>
-                                        <tableColumn identifier="ConversationsColumn" width="295" minWidth="40" maxWidth="1000" id="VDO-Cu-h8f">
+                                        <tableColumn identifier="ConversationsColumn" width="290" minWidth="40" maxWidth="1000" id="VDO-Cu-h8f">
                                             <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Conversations">
                                                 <font key="font" metaFont="smallSystem"/>
                                                 <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
@@ -57,7 +57,7 @@
                             <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
                         </clipView>
                         <constraints>
-                            <constraint firstAttribute="width" constant="300" id="eCz-d8-35b"/>
+                            <constraint firstAttribute="width" constant="295" id="eCz-d8-35b"/>
                             <constraint firstAttribute="height" constant="145" id="lWc-BG-yhJ"/>
                         </constraints>
                         <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="Fze-JQ-8rU">
@@ -73,18 +73,18 @@
                             <autoresizingMask key="autoresizingMask"/>
                         </tableHeaderView>
                     </scrollView>
-                    <tabView focusRingType="none" ambiguous="YES" misplaced="YES" type="bottomTabsBezelBorder" translatesAutoresizingMaskIntoConstraints="NO" id="xXW-iq-GcP">
+                    <tabView focusRingType="none" type="bottomTabsBezelBorder" translatesAutoresizingMaskIntoConstraints="NO" id="xXW-iq-GcP">
                         <rect key="frame" x="13" y="14" width="309" height="443"/>
                         <font key="font" metaFont="system"/>
                         <tabViewItems>
                             <tabViewItem label="History" identifier="1" id="Wi9-Zd-O1N">
-                                <view key="view" ambiguous="YES" id="h2E-qI-upQ">
-                                    <rect key="frame" x="10" y="7" width="289" height="405"/>
+                                <view key="view" id="h2E-qI-upQ">
+                                    <rect key="frame" x="10" y="7" width="295" height="405"/>
                                     <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                                     <subviews>
-                                        <scrollView ambiguous="YES" misplaced="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9uI-D5-KRt">
+                                        <scrollView misplaced="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9uI-D5-KRt">
                                             <rect key="frame" x="-3" y="0.0" width="295" height="408"/>
-                                            <clipView key="contentView" ambiguous="YES" misplaced="YES" id="eME-fQ-3QX">
+                                            <clipView key="contentView" misplaced="YES" id="eME-fQ-3QX">
                                                 <rect key="frame" x="1" y="17" width="238" height="117"/>
                                                 <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                                                 <subviews>
@@ -142,6 +142,9 @@
                                                 </subviews>
                                                 <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
                                             </clipView>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="295" id="x0a-p6-WiG"/>
+                                            </constraints>
                                             <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="9To-im-dx7">
                                                 <rect key="frame" x="1" y="119" width="223" height="15"/>
                                                 <autoresizingMask key="autoresizingMask"/>
@@ -157,10 +160,70 @@
                                         </scrollView>
                                     </subviews>
                                     <constraints>
-                                        <constraint firstAttribute="trailing" secondItem="9uI-D5-KRt" secondAttribute="trailing" constant="3" id="7Oo-Z3-JeE"/>
-                                        <constraint firstItem="9uI-D5-KRt" firstAttribute="top" secondItem="h2E-qI-upQ" secondAttribute="top" constant="3" id="Fu4-zc-WvO"/>
-                                        <constraint firstItem="9uI-D5-KRt" firstAttribute="leading" secondItem="h2E-qI-upQ" secondAttribute="leading" constant="3" id="mO1-Tn-dFB"/>
-                                        <constraint firstAttribute="bottom" secondItem="9uI-D5-KRt" secondAttribute="bottom" id="mRP-eB-8tk"/>
+                                        <constraint firstItem="9uI-D5-KRt" firstAttribute="leading" secondItem="h2E-qI-upQ" secondAttribute="leading" id="IKi-Kl-zdl"/>
+                                        <constraint firstItem="9uI-D5-KRt" firstAttribute="top" secondItem="h2E-qI-upQ" secondAttribute="top" id="SNf-F0-6HX"/>
+                                        <constraint firstAttribute="trailing" secondItem="9uI-D5-KRt" secondAttribute="trailing" id="adg-yP-eVY"/>
+                                        <constraint firstAttribute="bottom" secondItem="9uI-D5-KRt" secondAttribute="bottom" id="nLK-Pj-h7B"/>
+                                    </constraints>
+                                </view>
+                            </tabViewItem>
+                            <tabViewItem label="Contacts" identifier="" id="Zbi-X6-DLT">
+                                <view key="view" id="sag-tS-7Jw">
+                                    <rect key="frame" x="10" y="7" width="289" height="405"/>
+                                    <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                                    <subviews>
+                                        <scrollView focusRingType="none" misplaced="YES" autohidesScrollers="YES" horizontalLineScroll="48" horizontalPageScroll="10" verticalLineScroll="48" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rJv-ju-DFe">
+                                            <rect key="frame" x="-3" y="5" width="295" height="403"/>
+                                            <clipView key="contentView" focusRingType="none" misplaced="YES" id="S00-xr-jYM">
+                                                <rect key="frame" x="1" y="0.0" width="238" height="134"/>
+                                                <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                                                <subviews>
+                                                    <outlineView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" rowHeight="46" indentationPerLevel="16" autoresizesOutlineColumn="YES" outlineTableColumn="8Ve-L0-o7V" id="Hrg-Fe-uGq">
+                                                        <rect key="frame" x="0.0" y="0.0" width="293" height="48"/>
+                                                        <autoresizingMask key="autoresizingMask"/>
+                                                        <size key="intercellSpacing" width="3" height="2"/>
+                                                        <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+                                                        <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
+                                                        <tableColumns>
+                                                            <tableColumn identifier="NameColumn" width="289.7734375" minWidth="40" maxWidth="1000" id="8Ve-L0-o7V">
+                                                                <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
+                                                                    <font key="font" metaFont="system"/>
+                                                                    <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
+                                                                    <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
+                                                                </tableHeaderCell>
+                                                                <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="GAa-pQ-t4g" customClass="PersonCell">
+                                                                    <font key="font" metaFont="system"/>
+                                                                    <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
+                                                                    <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
+                                                                </textFieldCell>
+                                                                <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
+                                                            </tableColumn>
+                                                        </tableColumns>
+                                                        <connections>
+                                                            <outlet property="delegate" destination="9RF-6W-vEW" id="5bm-gu-2q9"/>
+                                                        </connections>
+                                                    </outlineView>
+                                                </subviews>
+                                                <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
+                                            </clipView>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="295" id="2Ia-36-f20"/>
+                                            </constraints>
+                                            <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="TGe-ww-q1j">
+                                                <rect key="frame" x="1" y="119" width="223" height="15"/>
+                                                <autoresizingMask key="autoresizingMask"/>
+                                            </scroller>
+                                            <scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="dD5-3G-tUh">
+                                                <rect key="frame" x="224" y="17" width="15" height="102"/>
+                                                <autoresizingMask key="autoresizingMask"/>
+                                            </scroller>
+                                        </scrollView>
+                                    </subviews>
+                                    <constraints>
+                                        <constraint firstAttribute="bottom" secondItem="rJv-ju-DFe" secondAttribute="bottom" constant="5" id="84Q-se-prS"/>
+                                        <constraint firstItem="rJv-ju-DFe" firstAttribute="leading" secondItem="sag-tS-7Jw" secondAttribute="leading" id="FHh-Sy-7VP"/>
+                                        <constraint firstItem="rJv-ju-DFe" firstAttribute="top" secondItem="sag-tS-7Jw" secondAttribute="top" id="VST-yM-sIz"/>
+                                        <constraint firstAttribute="trailing" secondItem="rJv-ju-DFe" secondAttribute="trailing" id="rZG-K9-Bc8"/>
                                     </constraints>
                                 </view>
                             </tabViewItem>
@@ -189,7 +252,7 @@
             <connections>
                 <outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
             </connections>
-            <point key="canvasLocation" x="530" y="361"/>
+            <point key="canvasLocation" x="529.5" y="361"/>
         </window>
         <viewController id="jzj-dD-ryc" customClass="HistoryViewController">
             <connections>
@@ -204,8 +267,11 @@
                 <outlet property="view" destination="tSW-YT-asL" id="fv5-ly-rk8"/>
             </connections>
         </viewController>
+        <viewController id="9RF-6W-vEW" customClass="PersonsVC">
+            <connections>
+                <outlet property="personsView" destination="Hrg-Fe-uGq" id="nMb-nK-g6a"/>
+                <outlet property="view" destination="rJv-ju-DFe" id="A8H-6d-bW2"/>
+            </connections>
+        </viewController>
     </objects>
-    <resources>
-        <image name="NSActionTemplate" width="14" height="14"/>
-    </resources>
 </document>
-- 
GitLab