ContactRequests: add Contact requests screen

ConversationModel: replaced Contact by ringId
Remove realm from Contact and Account (managed by daemon)
Pass reference to account list and current account to ContactsService
Add Invitations screen with Accept, Discard and Ban buttons
Add vCard load and save support

Change-Id: Ied42ef310af5e4849f4aef389584145a80e79888
parent dd026ff0
This diff is collapsed.
......@@ -19,7 +19,6 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import RealmSwift
import Foundation
/**
......@@ -32,14 +31,14 @@ enum AccountModelError: Error {
/**
A class representing an account.
*/
class AccountModel: Object {
class AccountModel: Equatable {
// MARK: Public members
dynamic var id: String = ""
dynamic var registeringUsername = false
dynamic var details: AccountConfigModel?
dynamic var volatileDetails: AccountConfigModel?
let credentialDetails = List<AccountCredentialsModel>()
let devices = List<DeviceModel>()
var id: String = ""
var registeringUsername = false
var details: AccountConfigModel?
var volatileDetails: AccountConfigModel?
var credentialDetails = [AccountCredentialsModel]()
var devices = [DeviceModel]()
// MARK: Init
convenience init(withAccountId accountId: String) {
......@@ -50,13 +49,13 @@ class AccountModel: Object {
convenience init(withAccountId accountId: String,
details: AccountConfigModel,
volatileDetails: AccountConfigModel,
credentials: List<AccountCredentialsModel>,
credentials: [AccountCredentialsModel],
devices: [DeviceModel]) throws {
self.init()
self.id = accountId
self.details = details
self.volatileDetails = volatileDetails
self.devices.append(objectsIn: devices)
self.devices.append(contentsOf: devices)
}
public static func == (lhs: AccountModel, rhs: AccountModel) -> Bool {
......
......@@ -24,6 +24,9 @@ import SwiftyBeaver
A structure exposing the fields and methods for an Account
*/
struct AccountModelHelper {
fileprivate static let ringIdPrefix = "ring:"
fileprivate var account: AccountModel
/**
......@@ -131,12 +134,15 @@ struct AccountModelHelper {
let accountUsernameKey = ConfigKeyModel(withKey: ConfigKey.accountUsername)
let accountUsername = self.account.details?.get(withConfigKeyModel: accountUsernameKey)
let ringIdPrefix = "ring:"
if accountUsername!.contains(ringIdPrefix) {
let index = accountUsername?.range(of: ringIdPrefix)?.upperBound
if accountUsername!.contains(AccountModelHelper.ringIdPrefix) {
let index = accountUsername?.range(of: AccountModelHelper.ringIdPrefix)?.upperBound
return accountUsername?.substring(from: index!)
} else {
return nil
}
}
static func uri(fromRingId ringId: String) -> String {
return self.ringIdPrefix.appending(ringId)
}
}
......@@ -25,6 +25,8 @@ import SwiftyBeaver
import RxSwift
import Chameleon
import Contacts
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
......@@ -33,6 +35,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
static let accountService = AccountsService(withAccountAdapter: AccountAdapter())
static let nameService = NameService(withNameRegistrationAdapter: NameRegistrationAdapter())
static let conversationsService = ConversationsService(withMessageAdapter: MessagesAdapter())
static let contactsService = ContactsService(withContactsAdapter: ContactsAdapter())
private let log = SwiftyBeaver.self
fileprivate let disposeBag = DisposeBag()
......@@ -53,6 +57,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Chameleon.setRingThemeUsingPrimaryColor(UIColor.ringMain, withSecondaryColor: UIColor.ringSecondary, andContentStyle: .light)
self.loadAccounts()
return true
}
......@@ -77,7 +82,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
do {
try AppDelegate.daemonService.startDaemon()
} catch StartDaemonError.initializationFailure {
log.error("Daemon failed to initialize.")
} catch StartDaemonError.startFailure {
......@@ -112,6 +116,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
if accountList.isEmpty {
self.presentWalkthrough()
} else {
AppDelegate.contactsService.loadContacts(withAccount: AppDelegate.accountService.currentAccount!)
AppDelegate.contactsService.loadContactRequests(withAccount: AppDelegate.accountService.currentAccount!)
self.presentMainTabBar()
}
}
......
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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.
*/
#import <Foundation/Foundation.h>
@protocol ContactsAdapterDelegate;
@interface ContactsAdapter : NSObject
@property (class, nonatomic, weak) id <ContactsAdapterDelegate> delegate;
//Contact Requests
- (NSArray<NSDictionary<NSString*,NSString*>*>*)trustRequestsWithAccountId:(NSString*)accountId;
- (BOOL)acceptTrustRequestFromContact:(NSString*)ringId withAccountId:(NSString*)accountId;
- (BOOL)discardTrustRequestFromContact:(NSString*)ringId withAccountId:(NSString*)accountId;
- (void)sendTrustRequestToContact:(NSString*)ringId payload:(NSData*)payload withAccountId:(NSString*)accountId;
//Contacts
- (void)addContactWithURI:(NSString*)uri accountId:(NSString*)accountId;
- (void)removeContactWithURI:(NSString*)uri accountId:(NSString*)accountId ban:(BOOL)ban;
- (NSDictionary*)contactDetailsWithURI:(NSString*)uri accountId:(NSString*)accountId;
- (NSArray<NSDictionary<NSString*,NSString*>*>*)contactsWithAccountId:(NSString*)accountId;
@end
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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.
*/
#import "ContactsAdapter.h"
#import "Utils.h"
#import "dring/configurationmanager_interface.h"
#import "Ring-Swift.h"
using namespace DRing;
@implementation ContactsAdapter
/// Static delegate that will receive the propagated daemon events
static id <ContactsAdapterDelegate> _delegate;
#pragma mark Init
- (id)init {
if (self = [super init]) {
[self registerConfigurationHandler];
}
return self;
}
#pragma mark -
#pragma mark Callbacks registration
- (void)registerConfigurationHandler {
std::map<std::string, std::shared_ptr<CallbackWrapperBase>> confHandlers;
//Incoming trust request signal
confHandlers.insert(exportable_callback<ConfigurationSignal::IncomingTrustRequest>([&](const std::string& account_id,
const std::string& from,
const std::vector<uint8_t>& payload,
time_t received) {
if(ContactsAdapter.delegate) {
NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
NSString* senderAccount = [NSString stringWithUTF8String:from.c_str()];
NSData* payloadData = [Utils dataFromVectorOfUInt8:payload];
NSDate* receivedDate = [NSDate dateWithTimeIntervalSince1970:received];
[ContactsAdapter.delegate incomingTrustRequestReceivedFrom:senderAccount
to:accountId
withPayload:payloadData
receivedDate:receivedDate];
}
}));
//Contact added signal
confHandlers.insert(exportable_callback<ConfigurationSignal::ContactAdded>([&](const std::string& account_id,
const std::string& uri,
bool confirmed) {
if(ContactsAdapter.delegate) {
NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
NSString* uriString = [NSString stringWithUTF8String:uri.c_str()];
[ContactsAdapter.delegate contactAddedWithContact:uriString withAccountId:accountId confirmed:(BOOL)confirmed];
}
}));
//Contact removed signal
confHandlers.insert(exportable_callback<ConfigurationSignal::ContactRemoved>([&](const std::string& account_id,
const std::string& uri,
bool banned) {
if(ContactsAdapter.delegate) {
NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
NSString* uriString = [NSString stringWithUTF8String:uri.c_str()];
[ContactsAdapter.delegate contactRemovedWithContact:uriString withAccountId:accountId banned:(BOOL)banned];
}
}));
registerConfHandlers(confHandlers);
}
#pragma mark -
//Contact Requests
- (NSArray<NSDictionary<NSString*,NSString*>*>*)trustRequestsWithAccountId:(NSString*)accountId {
std::vector<std::map<std::string,std::string>> trustRequestsVector = getTrustRequests(std::string([accountId UTF8String]));
NSArray* trustRequests = [Utils vectorOfMapsToArray:trustRequestsVector];
return trustRequests;
}
- (BOOL)acceptTrustRequestFromContact:(NSString*)ringId withAccountId:(NSString*)accountId {
return acceptTrustRequest(std::string([accountId UTF8String]), std::string([ringId UTF8String]));
}
- (BOOL)discardTrustRequestFromContact:(NSString*)ringId withAccountId:(NSString*)accountId {
return discardTrustRequest(std::string([accountId UTF8String]), std::string([ringId UTF8String]));
}
- (void)sendTrustRequestToContact:(NSString*)ringId payload:(NSData*)payloadData withAccountId:(NSString*)accountId {
std::vector<uint8_t> payload = [Utils vectorOfUInt8FromData:payloadData];
sendTrustRequest(std::string([accountId UTF8String]), std::string([ringId UTF8String]), payload);
}
//Contacts
- (void)addContactWithURI:(NSString*)uri accountId:(NSString*)accountId {
addContact(std::string([accountId UTF8String]), std::string([uri UTF8String]));
}
- (void)removeContactWithURI:(NSString*)uri accountId:(NSString*)accountId ban:(BOOL)ban {
removeContact(std::string([accountId UTF8String]), std::string([uri UTF8String]), (bool)ban);
}
- (NSDictionary*)contactDetailsWithURI:(NSString*)uri accountId:(NSString*)accountId {
std::map<std::string,std::string> contactDetails = getContactDetails(std::string([accountId UTF8String]), std::string([uri UTF8String]));
return [Utils mapToDictionnary:contactDetails];
}
- (NSArray<NSDictionary<NSString*,NSString*>*>*)contactsWithAccountId:(NSString*)accountId {
std::vector<std::map<std::string, std::string>> contacts = getContacts(std::string([accountId UTF8String]));
return [Utils vectorOfMapsToArray:contacts];
}
#pragma mark AccountAdapterDelegate
+ (id <ContactsAdapterDelegate>)delegate {
return _delegate;
}
+ (void) setDelegate:(id<ContactsAdapterDelegate>)delegate {
_delegate = delegate;
}
#pragma mark -
@end
......@@ -31,3 +31,4 @@
#import "NameRegistrationResponse.h"
#import "MessagesAdapter.h"
#import "Chameleon/Chameleon.h"
#import "ContactsAdapter.h"
......@@ -26,6 +26,8 @@
#import "dring/configurationmanager_interface.h"
#import "Utils.h"
@implementation SystemAdapter
using namespace DRing;
......
......@@ -32,5 +32,8 @@
(const std::map<std::string, std::string>&)map;
+ (std::map<std::string, std::string>)dictionnaryToMap:(NSDictionary*)dict;
+ (NSArray*)vectorOfMapsToArray:(const std::vector<std::map<std::string, std::string>>&)vectorOfMaps;
+ (NSData*)dataFromVectorOfUInt8:(std::vector<uint8_t>)vectorOfUInt8;
+ (std::vector<uint8_t>)vectorOfUInt8FromData:(NSData*)data;
@end
......@@ -69,4 +69,27 @@
return [NSArray arrayWithArray:array];
}
+ (NSData*)dataFromVectorOfUInt8:(std::vector<uint8_t>)vectorOfUInt8 {
NSMutableData* data = [[NSMutableData alloc] init];
std::for_each(vectorOfUInt8.begin(), vectorOfUInt8.end(), ^(uint8_t byte) {
[data appendBytes:&byte length:1];
});
return data;
}
+ (std::vector<uint8_t>)vectorOfUInt8FromData:(NSData*)data {
std::vector<uint8_t> vector;
char *bytes = (char*)data.bytes;
for ( int i = 0; i < data.length; i++ ) {
vector.push_back(bytes[i]);
}
return vector;
}
@end
......@@ -18,18 +18,33 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import RealmSwift
class ContactModel {
class ContactModel: Object {
var ringId: String = ""
var userName: String?
var confirmed: Bool = false
var added: Date = Date()
dynamic var ringId: String = ""
dynamic var userName: String?
convenience init(withRingId ringId: String) {
self.init()
init(withRingId ringId: String) {
self.ringId = ringId
}
init(withDictionary dictionary: [String : String]) {
if let ringId = dictionary["id"] {
self.ringId = ringId
}
if let confirmed = dictionary["confirmed"] {
self.confirmed = confirmed.toBool()!
}
if let added = dictionary["added"] {
let addedDate = Date(timeIntervalSince1970: Double(added)!)
self.added = addedDate
}
}
public static func == (lhs: ContactModel, rhs: ContactModel) -> Bool {
return lhs.ringId == rhs.ringId
}
......
......@@ -19,31 +19,20 @@
*/
import UIKit
import Reusable
import RxSwift
class ContactHelper {
static func lookupUserName(forRingId ringId: String, nameService: NameService, disposeBag: DisposeBag) -> Variable<String> {
class ContactRequestCell: UITableViewCell, NibReusable {
let userName = Variable("")
@IBOutlet weak var profileImageView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var acceptButton: UIButton!
@IBOutlet weak var discardButton: UIButton!
@IBOutlet weak var banButton: UIButton!
//Lookup the user name observer
nameService.usernameLookupStatus
.observeOn(MainScheduler.instance)
.filter({ lookupNameResponse in
return lookupNameResponse.address != nil && lookupNameResponse.address == ringId
}).subscribe(onNext: { lookupNameResponse in
if lookupNameResponse.state == .found {
userName.value = lookupNameResponse.name
} else {
userName.value = lookupNameResponse.address
}
}).disposed(by: disposeBag)
var disposeBag = DisposeBag()
nameService.lookupAddress(withAccount: "", nameserver: "", address: ringId)
return userName
override func prepareForReuse() {
self.disposeBag = DisposeBag()
}
}
This diff is collapsed.
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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.
*/
import RxSwift
import Contacts
import SwiftyBeaver
class ContactRequestItem {
let contactRequest: ContactRequestModel
let userName = Variable("")
let profileImageData: Data?
init(withContactRequest contactRequest: ContactRequestModel) {
self.contactRequest = contactRequest
self.profileImageData = self.contactRequest.vCard?.imageData
}
}
......@@ -18,63 +18,57 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import RxSwift
import RealmSwift
import Contacts
import SwiftyBeaver
class ContactViewModel {
class ContactRequestModel {
/**
logguer
*/
private let log = SwiftyBeaver.self
let ringId: String
let accountId: String
let vCard: CNContact?
let receivedDate: Date
private let nameService = AppDelegate.nameService
private let disposeBag = DisposeBag()
private let contact: ContactModel
private lazy var realm: Realm = {
guard let realm = try? Realm() else {
fatalError("Enable to instantiate Realm")
}
enum ContactRequestKey: String {
case from
case payload
case received
}
return realm
}()
private let log = SwiftyBeaver.self
let userName = Variable("")
init(withRingId ringId: String, vCard: CNContact?, receivedDate: Date, accountId: String) {
self.ringId = ringId
self.vCard = vCard
self.receivedDate = receivedDate
self.accountId = accountId
}
init(withContact contact: ContactModel) {
self.contact = contact
init(withDictionary dictionary: [String : String], accountId: String) {
if let userName = self.contact.userName {
self.userName.value = userName
if let ringId = dictionary[ContactRequestKey.from.rawValue] {
self.ringId = ringId
} else {
self.lookupUserName()
self.ringId = ""
}
}
func lookupUserName() {
nameService.usernameLookupStatus
.observeOn(MainScheduler.instance)
.filter({ [unowned self] lookupNameResponse in
return lookupNameResponse.address != nil && lookupNameResponse.address == self.contact.ringId
}).subscribe(onNext: { [unowned self] lookupNameResponse in
if lookupNameResponse.state == .found {
do {
try self.realm.write { [unowned self] in
self.contact.userName = lookupNameResponse.name
}
} catch let error {
self.log.error("Realm persistence with error: \(error)")
}
if let vCardString = dictionary[ContactRequestKey.payload.rawValue] {
do {
self.vCard = try CNContactVCardSerialization.contacts(with: vCardString.data(using: String.Encoding.utf8)!).first!
} catch {
log.error("Unable to serialize the vCard : \(error)")
self.vCard = CNContact()
}
} else {
self.vCard = nil
}
self.userName.value = lookupNameResponse.name
} else {
self.userName.value = lookupNameResponse.address
}
}).disposed(by: disposeBag)
if let receivedDateString = dictionary[ContactRequestKey.received.rawValue] {
let timestamp = Double(receivedDateString)
self.receivedDate = Date(timeIntervalSince1970: timestamp!)
} else {
self.receivedDate = Date()
}
nameService.lookupAddress(withAccount: "", nameserver: "", address: self.contact.ringId)
self.accountId = accountId
}
}
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@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.
*/
import UIKit
import RxSwift
import RxCocoa
import SwiftyBeaver
class ContactRequestsViewController: UIViewController {
fileprivate let viewModel = ContactRequestsViewModel(withContactsService: AppDelegate.contactsService,
accountsService: AppDelegate.accountService,
nameService: AppDelegate.nameService)
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var noInvitationsPlaceholder: UIView!
fileprivate let disposeBag = DisposeBag()
fileprivate let cellIdentifier = "ContactRequestCell"
fileprivate let log = SwiftyBeaver.self
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setupTableView()
self.setupBindings()
}
func setupTableView() {
self.tableView.estimatedRowHeight = 100.0
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.allowsSelection = false
//Register cell
self.tableView.register(cellType: ContactRequestCell.self)
//Bind the TableView to the ViewModel
self.viewModel
.contactRequestItems
.observeOn(MainScheduler.instance)
.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, cellType: ContactRequestCell.self)) { [unowned self] _, item, cell in
item.userName
.asObservable()
.observeOn(MainScheduler.instance)
.bind(to: cell.nameLabel.rx.text)
.disposed(by: cell.disposeBag)
if let imageData = item.profileImageData {
cell.profileImageView.image = UIImage(data: imageData)
}
//Accept button
cell.acceptButton.rx.tap.subscribe(onNext: { [unowned self] in
self.acceptButtonTapped(withItem: item)
}).disposed(by: cell.disposeBag)
//Discard button
cell.discardButton.rx.tap.subscribe(onNext: { [unowned self] in
self.discardButtonTapped(withItem: item)
}).disposed(by: cell.disposeBag)
//Ban button
cell.banButton.rx.tap.subscribe(onNext: { [unowned self] in
self.banButtonTapped(withItem: item)
}).disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
}
func setupBindings() {
self.viewModel
.hasInvitations
.observeOn(MainScheduler.instance)
.bind(to: self.noInvitationsPlaceholder.rx.isHidden)
.disposed(by: self.disposeBag)
}
func acceptButtonTapped(withItem item: ContactRequestItem) {
viewModel.accept(withItem: item).subscribe(onError: { [unowned self] error in
self.log.error("Accept trust request failed")
}, onCompleted: { [unowned self] in
self.log.info("Accept trust request done")
}).disposed(by: self.disposeBag)
}
func discardButtonTapped(withItem item: ContactRequestItem) {
viewModel.discard(withItem: item).subscribe(onError: { [unowned self] error in
self.log.error("Discard trust request failed")
}, onCompleted: { [unowned self] in
self.log.info("Discard trust request done")
}).disposed(by: self.disposeBag)
}
func banButtonTapped(withItem item: ContactRequestItem) {
viewModel.ban(withItem: item).subscribe(onError: { [unowned self] error in
self.log.error("Ban trust request failed")
}, onCompleted: { [unowned self] in
self.log.info("Ban trust request done")
}).disposed(by: self.disposeBag)
}
}
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.