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()
}
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12120" systemVersion="16A323" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="72" id="KZx-bh-W5G" customClass="ContactRequestCell" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="470" height="72"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KZx-bh-W5G" id="YU4-Oq-lYT">
<rect key="frame" x="0.0" y="0.0" width="470" height="71.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_contact_picture" translatesAutoresizingMaskIntoConstraints="NO" id="xS9-Kd-lrg">
<rect key="frame" x="16" y="16" width="40" height="40"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="width" constant="40" id="1np-vL-9Yu"/>
<constraint firstAttribute="height" constant="40" id="Ghu-V0-iZL"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="20"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="feR-9F-sZM">
<rect key="frame" x="236" y="37.5" width="70" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="70" id="F6k-BD-rff"/>
<constraint firstAttribute="height" constant="30" id="mgC-Pn-LLR"/>
</constraints>
<state key="normal" title="Accept">
<color key="titleColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/>
</state>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
<color key="value" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="5"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fWB-HR-tae">
<rect key="frame" x="310" y="37.5" width="70" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="70" id="BU4-ED-Mry"/>
<constraint firstAttribute="height" constant="30" id="fQt-vU-mXY"/>
</constraints>
<state key="normal" title="Discard">
<color key="titleColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/>
</state>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
<color key="value" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="5"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Pni-bm-rkr">
<rect key="frame" x="384" y="37.5" width="70" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="Bee-OT-mx8"/>
<constraint firstAttribute="width" constant="70" id="dsw-SF-fZe"/>
</constraints>
<state key="normal" title="Ban">
<color key="titleColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/>
</state>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
<color key="value" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
<real key="value" value="5"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Dla-OF-biH">
<rect key="frame" x="64" y="8.5" width="398" height="21"/>
<constraints>
<constraint firstAttribute="height" constant="21" id="rIm-Pj-KP3"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="Pni-bm-rkr" firstAttribute="top" secondItem="Dla-OF-biH" secondAttribute="bottom" constant="8" id="0jS-6Q-w8N"/>
<constraint firstItem="xS9-Kd-lrg" firstAttribute="centerY" secondItem="YU4-Oq-lYT" secondAttribute="centerY" id="DPx-jS-V0h"/>
<constraint firstItem="feR-9F-sZM" firstAttribute="top" secondItem="Dla-OF-biH" secondAttribute="bottom" constant="8" id="Gba-vQ-Ebp"/>
<constraint firstAttribute="bottom" secondItem="Pni-bm-rkr" secondAttribute="bottom" constant="4" id="KNE-9A-qWk"/>
<constraint firstItem="fWB-HR-tae" firstAttribute="top" secondItem="Dla-OF-biH" secondAttribute="bottom" constant="8" id="QMX-zx-zpZ"/>
<constraint firstItem="Pni-bm-rkr" firstAttribute="leading" secondItem="fWB-HR-tae" secondAttribute="trailing" constant="4" id="XcH-3n-PeT"/>
<constraint firstAttribute="bottom" secondItem="fWB-HR-tae" secondAttribute="bottom" constant="4" id="Z44-tV-yAn"/>
<constraint firstAttribute="bottom" secondItem="feR-9F-sZM" secondAttribute="bottom" constant="4" id="hAP-C0-nE5"/>
<constraint firstItem="Dla-OF-biH" firstAttribute="top" secondItem="YU4-Oq-lYT" secondAttribute="top" constant="8" id="hqf-Iv-xvb"/>
<constraint firstItem="fWB-HR-tae" firstAttribute="leading" secondItem="feR-9F-sZM" secondAttribute="trailing" constant="4" id="lfX-2s-AsZ"/>
<constraint firstItem="xS9-Kd-lrg" firstAttribute="leading" secondItem="YU4-Oq-lYT" secondAttribute="leading" constant="16" id="ogz-Qb-1Pz"/>
<constraint firstAttribute="trailing" secondItem="Pni-bm-rkr" secondAttribute="trailing" constant="16" id="sDu-vC-6gU"/>
<constraint firstAttribute="trailing" secondItem="Dla-OF-biH" secondAttribute="trailing" constant="8" id="wFU-JT-uD3"/>
<constraint firstItem="Dla-OF-biH" firstAttribute="leading" secondItem="xS9-Kd-lrg" secondAttribute="trailing" constant="8" id="zIp-sT-nb3"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="acceptButton" destination="feR-9F-sZM" id="N3u-Ww-sE5"/>
<outlet property="banButton" destination="Pni-bm-rkr" id="v1p-37-zfP"/>
<outlet property="discardButton" destination="fWB-HR-tae" id="ZMu-NC-hE4"/>
<outlet property="nameLabel" destination="Dla-OF-biH" id="tO5-og-I3P"/>
<outlet property="profileImageView" destination="xS9-Kd-lrg" id="Rxt-j1-fem"/>
</connections>
<point key="canvasLocation" x="177" y="-50"/>
</tableViewCell>
</objects>
<resources>
<image name="ic_contact_picture" width="128" height="128"/>
</resources>
</document>