Custom contact picker for invite flow

Preferred to the system contact picker because:
1. removes "group" clutter from header, unlikely to be used much.
2. can select while searching
3. fixes unified contact problem where e.g.
   If only one of your contact has a phone number, they appear disabled
   when choosing to invite via messaging, even though the other linked
   contact *does* have a phone number.
4. label users w/o email so it's clearer why they can't be selected

Also:

* Twitter share-image was too tall

// FREEBIE
This commit is contained in:
Michael Kirk 2016-11-23 11:07:38 -05:00
parent f9a60b622d
commit f30c733ef3
23 changed files with 750 additions and 122 deletions

View File

@ -134,7 +134,7 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
SignalServiceKit:
:commit: df756423f24ac91dd69f45cc036c09771c15f6eb
:commit: 3083e2929c7dcfe5c60e003dd77c310d26dab177
:git: https://github.com/WhisperSystems/SignalServiceKit.git
SocketRocket:
:commit: 41b57bb2fc292a814f758441a05243eb38457027

View File

@ -15,6 +15,9 @@
450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */; };
450873C81D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */; };
4516E3FF1DD2193B00DC4206 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */ = {isa = PBXBuildFile; fileRef = 4516E3FE1DD2193B00DC4206 /* OWS101ExistingUsersBlockOnIdentityChange.m */; };
451764271DE939F300EDB8B9 /* ContactsPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451764261DE939F300EDB8B9 /* ContactsPicker.swift */; };
4517642A1DE939FD00EDB8B9 /* ContactCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 451764281DE939FD00EDB8B9 /* ContactCell.xib */; };
4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451764291DE939FD00EDB8B9 /* ContactCell.swift */; };
451DE9F81DC18C9500810E42 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD81EE1DC030E7004C9430 /* AccountManager.swift */; };
451DE9FD1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451DE9FC1DC1A28200810E42 /* SyncPushTokensJob.swift */; };
451DE9FE1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451DE9FC1DC1A28200810E42 /* SyncPushTokensJob.swift */; };
@ -48,6 +51,7 @@
459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 459311FB1D75C948008DD4F0 /* OWSDeviceTableViewCell.m */; };
459C3F0D1C9B3A1B003ACF51 /* TSMessageAdapterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 459C3F0C1C9B3A1B003ACF51 /* TSMessageAdapterTest.m */; };
45B201761DAECBFE00C461E0 /* HighlightableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B201751DAECBFE00C461E0 /* HighlightableLabel.swift */; };
45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45BD60811DE9547E00A8F436 /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
45BFFFA81D898AF0004A12A7 /* OWSStaleNotificationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 45BFFFA71D898AF0004A12A7 /* OWSStaleNotificationObserver.m */; };
45BFFFA91D898AF0004A12A7 /* OWSStaleNotificationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 45BFFFA71D898AF0004A12A7 /* OWSStaleNotificationObserver.m */; };
45C681B71D305A580050903A /* OWSCall.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C681B61D305A580050903A /* OWSCall.m */; };
@ -551,6 +555,9 @@
450873C91D9D86F4006B54F2 /* OWSExpirableMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirableMessageView.h; sourceTree = "<group>"; };
4516E3FD1DD2193B00DC4206 /* OWS101ExistingUsersBlockOnIdentityChange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWS101ExistingUsersBlockOnIdentityChange.h; path = Migrations/OWS101ExistingUsersBlockOnIdentityChange.h; sourceTree = "<group>"; };
4516E3FE1DD2193B00DC4206 /* OWS101ExistingUsersBlockOnIdentityChange.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWS101ExistingUsersBlockOnIdentityChange.m; path = Migrations/OWS101ExistingUsersBlockOnIdentityChange.m; sourceTree = "<group>"; };
451764261DE939F300EDB8B9 /* ContactsPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsPicker.swift; sourceTree = "<group>"; };
451764281DE939FD00EDB8B9 /* ContactCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactCell.xib; sourceTree = "<group>"; };
451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = "<group>"; };
451DE9F11DC1585F00810E42 /* PromiseKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PromiseKit.framework; path = Carthage/Build/iOS/PromiseKit.framework; sourceTree = "<group>"; };
451DE9FC1DC1A28200810E42 /* SyncPushTokensJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SyncPushTokensJob.swift; path = Models/SyncPushTokensJob.swift; sourceTree = "<group>"; };
4520D8D41D417D8E00123472 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; };
@ -602,6 +609,7 @@
459C3F0C1C9B3A1B003ACF51 /* TSMessageAdapterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TSMessageAdapterTest.m; path = "view controllers/Signals/TSMessageAdapters/TSMessageAdapterTest.m"; sourceTree = "<group>"; };
45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Signal-Bridging-Header.h"; sourceTree = "<group>"; };
45B201751DAECBFE00C461E0 /* HighlightableLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightableLabel.swift; sourceTree = "<group>"; };
45BD60811DE9547E00A8F436 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; };
45BFFFA61D898AF0004A12A7 /* OWSStaleNotificationObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSStaleNotificationObserver.h; path = Observers/OWSStaleNotificationObserver.h; sourceTree = "<group>"; };
45BFFFA71D898AF0004A12A7 /* OWSStaleNotificationObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSStaleNotificationObserver.m; path = Observers/OWSStaleNotificationObserver.m; sourceTree = "<group>"; };
45C681B51D305A580050903A /* OWSCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSCall.h; sourceTree = "<group>"; };
@ -1153,6 +1161,7 @@
456C38961DC7B882007536A7 /* PromiseKit.framework in Frameworks */,
4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */,
B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */,
45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */,
B6FE7EB71ADD62FA00A6D22F /* PushKit.framework in Frameworks */,
FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */,
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */,
@ -1826,6 +1835,7 @@
76EB050C18170B33006006FC /* InCallViewController.m */,
458E382F1D6682450094BD24 /* OWSQRCodeScanningViewController.h */,
458E38301D6682450094BD24 /* OWSQRCodeScanningViewController.m */,
451764261DE939F300EDB8B9 /* ContactsPicker.swift */,
45514DE11DDFA183003EFF90 /* InviteFlow.swift */,
);
name = "View Controllers";
@ -1854,6 +1864,8 @@
FCAC963E19FEF99A0046DFC5 /* InboxTableViewCell.m */,
76EB052E18170B33006006FC /* ContactTableViewCell.h */,
76EB052F18170B33006006FC /* ContactTableViewCell.m */,
451764291DE939FD00EDB8B9 /* ContactCell.swift */,
451764281DE939FD00EDB8B9 /* ContactCell.xib */,
76EB053818170B33006006FC /* xibs */,
459311FA1D75C948008DD4F0 /* OWSDeviceTableViewCell.h */,
459311FB1D75C948008DD4F0 /* OWSDeviceTableViewCell.m */,
@ -2233,6 +2245,7 @@
D221A08C169C9E5E00537ABF /* Frameworks */ = {
isa = PBXGroup;
children = (
45BD60811DE9547E00A8F436 /* Contacts.framework */,
451DE9F11DC1585F00810E42 /* PromiseKit.framework */,
4520D8D41D417D8E00123472 /* Photos.framework */,
B6B226961BE4B7D200860F4D /* ContactsUI.framework */,
@ -2651,6 +2664,7 @@
AD83FF401A73426500B5C81A /* audio_pause_button_blue@2x.png in Resources */,
B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */,
70B8FEE21909FE360042E3F0 /* 171756__nenadsimic__picked-coin-echo-2.wav in Resources */,
4517642A1DE939FD00EDB8B9 /* ContactCell.xib in Resources */,
AD83FF431A73426500B5C81A /* audio_play_button@2x.png in Resources */,
45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */,
B633C5C31A1D190B0059AC12 /* mute_off@2x.png in Resources */,
@ -2926,6 +2940,8 @@
76EB05EA18170B33006006FC /* CallProgress.m in Sources */,
458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */,
FCFA64B41A24F3880007FB87 /* UIColor+OWS.m in Sources */,
4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */,
451764271DE939F300EDB8B9 /* ContactsPicker.swift in Sources */,
76EB05C218170B33006006FC /* DhPacketSharedSecretHashes.m in Sources */,
B6C93C4E199567AD00EDF894 /* DebugLogger.m in Sources */,
76EB063218170B33006006FC /* Crc32.m in Sources */,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -38,7 +38,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>2.6.6.5</string>
<string>2.6.6.6</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LOGS_EMAIL</key>

View File

@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN
@class TSThread;
@class OWSContactsManager;
@class UIImage;
@interface OWSAvatarBuilder : NSObject

View File

@ -3,11 +3,17 @@
//
#import <Foundation/Foundation.h>
#import "Environment.h"
#import "OWSContactAvatarBuilder.h"
#import "OWSContactsManager.h"
#import "OWSLogger.h"
#import "PhoneNumber.h"
#import "PropertyListPreferences.h"
#import "PushManager.h"
#import "RPAccountManager.h"
#import "UIFont+OWS.h"
#import "UIUtil.h"
#import <SignalServiceKit/Contact.h>
#import <SignalServiceKit/NSDate+millisecondTimeStamp.h>
#import <SignalServiceKit/OWSEndSessionMessage.h>
#import <SignalServiceKit/OWSError.h>

View File

@ -28,7 +28,7 @@
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No Messages :( Tap Compose to send a message or invite a friend to Signal" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Srx-i1-WhD">
<rect key="frame" x="37.5" y="106.5" width="300" height="200"/>
<rect key="frame" x="38" y="107" width="300" height="200"/>
<constraints>
<constraint firstAttribute="height" constant="200" id="GEd-dY-d8r"/>
<constraint firstAttribute="width" constant="300" id="siA-1a-pO1"/>
@ -143,10 +143,10 @@
<blurEffect style="dark"/>
</visualEffectView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8DU-2J-HAz" userLabel="Scanner Container">
<rect key="frame" x="0.0" y="-333.5" width="375" height="333.5"/>
<rect key="frame" x="0.0" y="-334" width="375" height="334"/>
<subviews>
<containerView hidden="YES" opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="MhA-kC-nBb" userLabel="Scanner">
<rect key="frame" x="0.0" y="64" width="375" height="269.5"/>
<rect key="frame" x="0.0" y="64" width="375" height="270"/>
<connections>
<segue destination="EFG-13-FgR" kind="embed" identifier="embedIdentityQRScanner" id="0sF-Is-2kw"/>
</connections>
@ -160,19 +160,19 @@
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="b7W-j4-3S1" userLabel="QR Container">
<rect key="frame" x="0.0" y="0.0" width="375" height="333.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="334"/>
<subviews>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Scan the QR Code on your contact's device." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="NXP-Ue-iml">
<rect key="frame" x="16" y="16" width="343" height="20.5"/>
<rect key="frame" x="16" y="16" width="343" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.94901960780000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lA7-b4-o6C" userLabel="QR Frame">
<rect key="frame" x="61.5" y="72" width="253.5" height="253.5"/>
<rect key="frame" x="60.5" y="72" width="254" height="254"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="btnQRShow--white" highlightedImage="btnQRShow--white-1" translatesAutoresizingMaskIntoConstraints="NO" id="YOs-e5-ee3" userLabel="Privacy Verification QR">
<rect key="frame" x="41.5" y="43" width="169" height="168.5"/>
<rect key="frame" x="42.5" y="43" width="169" height="169"/>
<constraints>
<constraint firstAttribute="width" secondItem="YOs-e5-ee3" secondAttribute="height" multiplier="1:1" id="lN0-BE-w2c"/>
</constraints>
@ -198,7 +198,7 @@
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="B4o-Rc-z68" userLabel="Instructions Container">
<rect key="frame" x="0.0" y="333.5" width="375" height="289.5"/>
<rect key="frame" x="0.0" y="334" width="375" height="289"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="12345 54432 83456 89456 24327 87547 90123 31523 91052 84930 89304 00234" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e7E-iS-3Oc" userLabel="Privacy Verification Key Text" customClass="OWSHighlightableLabel">
<rect key="frame" x="36" y="0.0" width="303" height="70"/>
@ -216,7 +216,7 @@
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Scan the code on your contact's device, or ask them to scan your code to verify that your messages are end-to-end encrypted." lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DV4-GV-ZPf" userLabel="Instructions ">
<rect key="frame" x="16" y="78" width="343" height="109.5"/>
<rect key="frame" x="16" y="78" width="343" height="109"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="44" id="bif-t1-qQO"/>
</constraints>
@ -225,7 +225,7 @@
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="RX6-CJ-FV1">
<rect key="frame" x="117" y="207.5" width="141" height="70"/>
<rect key="frame" x="117" y="207" width="141" height="70"/>
<constraints>
<constraint firstAttribute="height" constant="70" id="g0E-XL-o8K"/>
</constraints>
@ -342,13 +342,13 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Edward Snowden" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qCt-Cp-bFX" userLabel="Name">
<rect key="frame" x="84" y="15" width="259" height="26.5"/>
<rect key="frame" x="84" y="15" width="259" height="27"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="+1 323-555-1234" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tmV-r0-09O" userLabel="Signal ID">
<rect key="frame" x="84" y="41.5" width="259" height="18"/>
<rect key="frame" x="84" y="42" width="259" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -407,7 +407,7 @@
<rect key="frame" x="0.0" y="144" width="375" height="108"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="U6h-Xo-HEv" id="Nmz-2u-fOY">
<rect key="frame" x="0.0" y="0.0" width="375" height="107"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="107.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Disappearing Messages" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="qbY-qJ-enK" userLabel="Disappearing Messages">
@ -420,7 +420,7 @@
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0dO-mx-W3I" userLabel="Disappearing Messages Switch">
<rect key="frame" x="310" y="6.5" width="51" height="31"/>
<rect key="frame" x="310" y="7" width="51" height="31"/>
<connections>
<action selector="disappearingMessagesSwitchValueDidChange:" destination="4oU-Rv-yJi" eventType="valueChanged" id="3fF-JA-69e"/>
</connections>
@ -434,7 +434,7 @@
</constraints>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="When enabled, messages sent and received in this conversation will disappear after they have been seen." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="zIg-K4-8lV">
<rect key="frame" x="64" y="40" width="287" height="59.5"/>
<rect key="frame" x="64" y="40" width="287" height="60"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -460,7 +460,7 @@
<rect key="frame" x="0.0" y="252" width="375" height="76"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="3dL-aW-P1A" id="2a2-Po-p8O">
<rect key="frame" x="0.0" y="0.0" width="375" height="75"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="75.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Messages disappear after 8 hours." lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="c6Q-rV-1LO" userLabel="Keep messages for 8 hours.">
@ -506,14 +506,14 @@
<tableViewSection headerTitle="Group Management" id="z5m-Fe-GK8">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="NxZ-wa-xV9" style="IBUITableViewCellStyleDefault" id="XHr-b6-Gvn" userLabel="Update Group">
<rect key="frame" x="0.0" y="385" width="375" height="44"/>
<rect key="frame" x="0.0" y="384" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="XHr-b6-Gvn" id="Epj-vT-UYL">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Update Group" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="NxZ-wa-xV9">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@ -526,14 +526,14 @@
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="geN-YN-TQg" style="IBUITableViewCellStyleDefault" id="w57-rz-BWN" userLabel="Leave Group">
<rect key="frame" x="0.0" y="429" width="375" height="44"/>
<rect key="frame" x="0.0" y="428" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="w57-rz-BWN" id="Pgy-Fc-U25">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Leave Group" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="geN-YN-TQg">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@ -543,14 +543,14 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="Zml-Zn-2fd" style="IBUITableViewCellStyleDefault" id="Dnq-Ko-46l" userLabel="Group Members">
<rect key="frame" x="0.0" y="473" width="375" height="44"/>
<rect key="frame" x="0.0" y="472" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Dnq-Ko-46l" id="VRQ-31-E5Y">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Group Members" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Zml-Zn-2fd">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@ -608,7 +608,7 @@
<rect key="frame" x="0.0" y="22" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hyn-Ss-OAa" id="4XE-JO-Upr">
<rect key="frame" x="0.0" y="0.0" width="375" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
@ -769,7 +769,7 @@
<rect key="frame" x="0.0" y="567" width="375" height="100"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="F2a-2h-UK2" userLabel="Hang Up Button">
<rect key="frame" x="147.5" y="0.0" width="80" height="80"/>
<rect key="frame" x="148" y="0.0" width="80" height="80"/>
<constraints>
<constraint firstAttribute="width" constant="80" id="ToV-Ks-2i9"/>
<constraint firstAttribute="height" constant="80" id="ifY-o4-2Pv"/>
@ -906,14 +906,14 @@
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="ExistingDevice" rowHeight="72" id="XjV-oU-jSb" customClass="OWSDeviceTableViewCell">
<rect key="frame" x="0.0" y="56" width="375" height="72"/>
<rect key="frame" x="0.0" y="55.5" width="375" height="72"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="XjV-oU-jSb" id="XqL-QG-IbY">
<rect key="frame" x="0.0" y="0.0" width="375" height="71"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="71.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Signal on Chrome" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="57o-uV-YOg">
<rect key="frame" x="17" y="8" width="135.5" height="20"/>
<rect key="frame" x="17" y="8" width="136" height="20"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@ -925,7 +925,7 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Last Seen: Aug 25, 2016" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Kek-MK-oLy">
<rect key="frame" x="17" y="46" width="147.5" height="17.5"/>
<rect key="frame" x="17" y="46" width="148" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@ -950,21 +950,21 @@
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="AddNewDevice" textLabel="w80-IJ-E6R" detailTextLabel="8ft-2u-wBF" style="IBUITableViewCellStyleSubtitle" id="6h2-gg-1C6">
<rect key="frame" x="0.0" y="128" width="375" height="44"/>
<rect key="frame" x="0.0" y="127.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="6h2-gg-1C6" id="RKi-c6-pzb">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Link New Device" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="w80-IJ-E6R">
<rect key="frame" x="15" y="3" width="127" height="21"/>
<rect key="frame" x="15" y="4" width="127" height="20.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Scan QR Code" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="8ft-2u-wBF">
<rect key="frame" x="15" y="24" width="88" height="16"/>
<rect key="frame" x="15" y="24.5" width="88" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -1019,7 +1019,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="5zF-Ko-9qU" id="gr7-Sm-bcs">
<rect key="frame" x="0.0" y="0.0" width="375" height="95"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="95.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1 (708) 000-1234" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ipE-BI-sLL">
@ -1060,11 +1060,11 @@
<rect key="frame" x="0.0" y="96" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="8rk-06-1ZS" id="hqv-P5-du9">
<rect key="frame" x="0.0" y="0.0" width="375" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Network Status" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uNq-FV-lwt">
<rect key="frame" x="15" y="-0.5" width="200" height="44.5"/>
<rect key="frame" x="15" y="-0.5" width="200" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="nOw-0c-lAd"/>
<constraint firstAttribute="width" constant="200" id="q6L-Sa-lrA"/>
@ -1074,7 +1074,7 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Connected" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tg3-dQ-odw">
<rect key="frame" x="260" y="-0.5" width="100" height="44.5"/>
<rect key="frame" x="260" y="-0.5" width="100" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="Lw2-Fv-sOC"/>
<constraint firstAttribute="height" constant="44" id="uvH-QZ-iUw"/>
@ -1100,11 +1100,11 @@
<rect key="frame" x="0.0" y="140" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="a7a-qe-87x" id="gcU-Ld-alv">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Invite Your Friends" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="9FD-M0-WLe" userLabel="Invite Your Friends">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@ -1117,11 +1117,11 @@
<rect key="frame" x="0.0" y="184" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="ITG-sW-Zn0" id="vOb-SA-SH2">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Privacy" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="i1f-DT-7rL">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -1134,11 +1134,11 @@
<rect key="frame" x="0.0" y="228" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="jp5-vZ-AhJ" id="sji-CJ-bhq">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Notifications" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tRQ-1p-6aT">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -1151,11 +1151,11 @@
<rect key="frame" x="0.0" y="272" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="wZ8-fs-Ylw" id="Ua5-nw-s2z">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Linked Devices" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hrc-lZ-NeA">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -1171,11 +1171,11 @@
<rect key="frame" x="0.0" y="316" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Xx7-pz-aLN" id="pMA-vR-8Ae">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Advanced" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="dkL-Nz-E6H">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -1188,11 +1188,11 @@
<rect key="frame" x="0.0" y="360" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="EI4-kQ-MMA" id="czg-5p-aVz">
<rect key="frame" x="0.0" y="0.0" width="342" height="43"/>
<rect key="frame" x="0.0" y="0.0" width="342" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="About" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="qeN-f1-cIQ">
<rect key="frame" x="15" y="0.0" width="325" height="43"/>
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -1209,7 +1209,7 @@
<rect key="frame" x="0.0" y="404" width="375" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="R82-FT-SEA" id="Ok9-fE-WhB">
<rect key="frame" x="0.0" y="0.0" width="375" height="99"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="99.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="4Mk-ly-6fq">
@ -1295,18 +1295,18 @@
</constraints>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_devices_ios" translatesAutoresizingMaskIntoConstraints="NO" id="zl7-KG-ma4">
<rect key="frame" x="86.5" y="64.5" width="171.5" height="114"/>
<rect key="frame" x="86" y="64.5" width="171" height="114"/>
<color key="tintColor" red="0.67450980392156867" green="0.67450980392156867" blue="0.67450980392156867" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" secondItem="zl7-KG-ma4" secondAttribute="height" multiplier="3:2" id="RKt-hc-iWB"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="GMf-cM-kQN" userLabel="Spacer">
<rect key="frame" x="0.0" y="215" width="343" height="64.5"/>
<rect key="frame" x="0.0" y="215.5" width="343" height="64"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Scan the QR code displayed on the device to link." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="3D8-yn-TJ8">
<rect key="frame" x="0.0" y="194.5" width="343" height="20.5"/>
<rect key="frame" x="0.0" y="194.5" width="343" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@ -1390,32 +1390,34 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="ContactTableViewCell" rowHeight="48" id="Ld5-sX-pB8" customClass="ContactTableViewCell">
<rect key="frame" x="0.0" y="22" width="375" height="48"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="ContactTableViewCell" rowHeight="59" id="Ld5-sX-pB8" customClass="ContactTableViewCell">
<rect key="frame" x="0.0" y="22" width="375" height="59"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Ld5-sX-pB8" id="EqP-87-4hZ">
<rect key="frame" x="0.0" y="0.0" width="375" height="47"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="58.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4zF-SU-q4z">
<rect key="frame" x="67.5" y="8" width="299.5" height="31.5"/>
<rect key="frame" x="68" y="8.5" width="299" height="42"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="empty-group-avatar" translatesAutoresizingMaskIntoConstraints="NO" id="11n-jp-FCg">
<rect key="frame" x="16" y="4" width="39.5" height="39.5"/>
<rect key="frame" x="16" y="9.5" width="40" height="40"/>
<constraints>
<constraint firstAttribute="width" secondItem="11n-jp-FCg" secondAttribute="height" multiplier="1:1" id="CMp-Im-YMw"/>
<constraint firstAttribute="height" constant="40" id="QYQ-Ma-4Vc"/>
<constraint firstAttribute="width" constant="40" id="jlW-F8-wog"/>
</constraints>
</imageView>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="4zF-SU-q4z" secondAttribute="trailing" id="0Af-5G-21G"/>
<constraint firstItem="11n-jp-FCg" firstAttribute="leading" secondItem="EqP-87-4hZ" secondAttribute="leadingMargin" constant="8" id="GYw-2Z-sxC"/>
<constraint firstAttribute="bottomMargin" secondItem="11n-jp-FCg" secondAttribute="bottom" constant="-4" id="XTB-Kg-dNU"/>
<constraint firstItem="4zF-SU-q4z" firstAttribute="leading" secondItem="11n-jp-FCg" secondAttribute="trailing" constant="12" id="ZiW-nb-biK"/>
<constraint firstItem="11n-jp-FCg" firstAttribute="top" secondItem="EqP-87-4hZ" secondAttribute="topMargin" constant="-4" id="pYu-uo-erC"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="11n-jp-FCg" secondAttribute="bottom" constant="6" id="d2J-3C-boc"/>
<constraint firstItem="11n-jp-FCg" firstAttribute="top" relation="greaterThanOrEqual" secondItem="EqP-87-4hZ" secondAttribute="top" constant="6" id="f2o-9N-mNq"/>
<constraint firstItem="11n-jp-FCg" firstAttribute="centerY" secondItem="EqP-87-4hZ" secondAttribute="centerY" id="kje-lH-syk"/>
<constraint firstAttribute="bottomMargin" secondItem="4zF-SU-q4z" secondAttribute="bottom" id="qZb-xR-JoS"/>
<constraint firstItem="4zF-SU-q4z" firstAttribute="top" secondItem="EqP-87-4hZ" secondAttribute="topMargin" id="ruR-db-L1p"/>
</constraints>
@ -1463,7 +1465,7 @@
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Invite your friends!" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QGl-1D-W9D">
<rect key="frame" x="15" y="0.0" width="325" height="47"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.09412795243864841" green="0.43645224658557435" blue="0.71380208333333339" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
@ -1548,11 +1550,11 @@
<rect key="frame" x="0.0" y="62" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="yfF-Jl-bZ1" id="f0v-od-N9K">
<rect key="frame" x="0.0" y="0.0" width="375" height="59"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="59.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="a4j-OQ-ala">
<rect key="frame" x="15" y="0.0" width="345" height="59"/>
<rect key="frame" x="15" y="0.0" width="345" height="59.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="20"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -1625,7 +1627,7 @@
<viewControllerLayoutGuide type="bottom" id="w3c-y6-c5W"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Z7D-4g-GIf">
<rect key="frame" x="0.0" y="0.0" width="375" height="269.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="270"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>

View File

@ -347,7 +347,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
}];
}
- (NSArray<Contact *> *)allContacts {
- (NSArray<Contact *> *_Nonnull)allContacts {
NSMutableArray *allContacts = [NSMutableArray array];
for (NSString *key in self.latestContactsById.allKeys) {

View File

@ -32,6 +32,7 @@ static NSString *const kCallSegue = @"2.0_6.0_Call_Segue";
@class ContactsUpdater;
@class TSNetworkManager;
@class OWSMessageSender;
@class UINavigationController;
@interface Environment : NSObject

View File

@ -0,0 +1,379 @@
// Originally based on EPContacts
//
// Created by Prabaharan Elangovan on 12/10/15.
// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved.
//
// Modified for Signal by Michael Kirk on 11/25/2016
// Parts Copyright © 2016 Open Whisper Systems. All rights reserved.
import UIKit
import Contacts
@available(iOS 9.0, *)
public protocol ContactsPickerDelegate {
func contactsPicker(_: ContactsPicker, didContactFetchFailed error: NSError)
func contactsPicker(_: ContactsPicker, didCancel error: NSError)
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact)
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact])
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
}
@available(iOS 9.0, *)
public extension ContactsPickerDelegate {
func contactsPicker(_: ContactsPicker, didContactFetchFailed error: NSError) { }
func contactsPicker(_: ContactsPicker, didCancel error: NSError) { }
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) { }
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) { }
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool { return true }
}
public enum SubtitleCellValue{
case phoneNumber
case email
}
@available(iOS 9.0, *)
open class ContactsPicker: UITableViewController, UISearchResultsUpdating, UISearchBarDelegate {
// MARK: - Properties
let TAG = "[ContactsPicker]"
let contactCellReuseIdentifier = "contactCellReuseIdentifier"
let contactsManager: OWSContactsManager
let collation = UILocalizedIndexedCollation.current()
let contactStore = CNContactStore()
lazy var resultSearchController = UISearchController()
// Data Source State
lazy var sections = [[CNContact]]()
lazy var filteredSections = [[CNContact]]()
lazy var selectedContacts = [Contact]()
// Configuration
open var contactsPickerDelegate: ContactsPickerDelegate?
var subtitleCellValue = SubtitleCellValue.phoneNumber
var multiSelectEnabled = false
let allowedContactKeys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactThumbnailImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor
]
// MARK: - Lifecycle Methods
override open func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
// Don't obscure table header (search bar) with table index
tableView.sectionIndexBackgroundColor = UIColor.clear
// Auto size cells for dynamic type
tableView.estimatedRowHeight = 60.0
tableView.rowHeight = UITableViewAutomaticDimension
tableView.allowsMultipleSelection = multiSelectEnabled
registerContactCell()
initializeBarButtons()
reloadContacts()
initializeSearchBar()
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
}
func didChangePreferredContentSize() {
self.tableView.reloadData()
}
func initializeSearchBar() {
self.resultSearchController = ( {
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = self
controller.dimsBackgroundDuringPresentation = false
controller.searchBar.sizeToFit()
controller.searchBar.delegate = self
controller.hidesNavigationBarDuringPresentation = false
self.tableView.tableHeaderView = controller.searchBar
return controller
})()
}
func initializeBarButtons() {
let cancelButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.cancel, target: self, action: #selector(onTouchCancelButton))
self.navigationItem.leftBarButtonItem = cancelButton
if multiSelectEnabled {
let doneButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.done, target: self, action: #selector(onTouchDoneButton))
self.navigationItem.rightBarButtonItem = doneButton
}
}
fileprivate func registerContactCell() {
tableView.register(ContactCell.nib, forCellReuseIdentifier: contactCellReuseIdentifier)
}
// MARK: - Initializers
override init(style: UITableViewStyle) {
contactsManager = Environment.getCurrent().contactsManager
super.init(style: style)
}
required public init?(coder aDecoder: NSCoder) {
contactsManager = Environment.getCurrent().contactsManager
super.init(coder: aDecoder)
}
convenience public init(delegate: ContactsPickerDelegate?) {
self.init(delegate: delegate, multiSelection: false)
}
convenience public init(delegate: ContactsPickerDelegate?, multiSelection : Bool) {
self.init(style: .plain)
multiSelectEnabled = multiSelection
contactsPickerDelegate = delegate
}
convenience public init(delegate: ContactsPickerDelegate?, multiSelection : Bool, subtitleCellType: SubtitleCellValue) {
self.init(style: .plain)
multiSelectEnabled = multiSelection
contactsPickerDelegate = delegate
subtitleCellValue = subtitleCellType
}
// MARK: - Contact Operations
open func reloadContacts() {
getContacts( onError: { error in
Logger.error("\(self.TAG) failed to reload contacts with error:\(error)")
})
}
func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
let title = NSLocalizedString("AB_PERMISSION_MISSING_TITLE", comment: "Alert title when contacts disabled")
let body = NSLocalizedString("ADDRESSBOOK_RESTRICTED_ALERT_BODY", comment: "Alert body when contacts disabled")
let alert = UIAlertController(title: title, message: body, preferredStyle: UIAlertControllerStyle.alert)
let dismissText = NSLocalizedString("DISMISS_BUTTON_TEXT", comment:"")
let okAction = UIAlertAction(title: dismissText, style: UIAlertActionStyle.default, handler: { action in
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
self.contactsPickerDelegate?.contactsPicker(self, didContactFetchFailed: error)
errorHandler(error)
self.dismiss(animated: true, completion: nil)
})
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
case CNAuthorizationStatus.notDetermined:
//This case means the user is prompted for the first time for allowing contacts
contactStore.requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in
//At this point an alert is provided to the user to provide access to contacts. This will get invoked if a user responds to the alert
if granted {
self.getContacts(onError: errorHandler)
} else {
errorHandler(error!)
}
}
case CNAuthorizationStatus.authorized:
//Authorization granted by user for this app.
var contacts = [CNContact]()
do {
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, stop) -> Void in
contacts.append(contact)
}
self.sections = collatedContacts(contacts)
self.tableView.reloadData()
} catch let error as NSError {
Logger.error("\(self.TAG) Failed to fetch contacts with error:\(error)")
}
}
}
func collatedContacts(_ contacts: [CNContact]) -> [[CNContact]] {
let selector: Selector = #selector(getter: CNContact.nameForCollating)
var collated = Array(repeating: [CNContact](), count: collation.sectionTitles.count)
for contact in contacts {
let sectionNumber = collation.section(for: contact, collationStringSelector: selector)
collated[sectionNumber].append(contact)
}
return collated
}
// MARK: - Table View DataSource
override open func numberOfSections(in tableView: UITableView) -> Int {
return self.collation.sectionTitles.count
}
override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let dataSource = resultSearchController.isActive ? filteredSections : sections
return dataSource[section].count
}
// MARK: - Table View Delegates
override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as! ContactCell
let dataSource = resultSearchController.isActive ? filteredSections : sections
let cnContact = dataSource[indexPath.section][indexPath.row]
let contact = Contact(contact: cnContact)
cell.updateContactsinUI(contact, subtitleType: subtitleCellValue, contactsManager: self.contactsManager)
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
cell.isSelected = isSelected
// Make sure we preserve selection across tableView.reloadData which happens when toggling between
// search controller
if (isSelected) {
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
} else {
self.tableView.deselectRow(at: indexPath, animated: false)
}
return cell
}
override open func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
let deselectedContact = cell.contact!
selectedContacts = selectedContacts.filter() {
return $0.uniqueId != deselectedContact.uniqueId
}
}
override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
let selectedContact = cell.contact!
guard (contactsPickerDelegate == nil || contactsPickerDelegate!.contactsPicker(self, shouldSelectContact: selectedContact)) else {
self.tableView.deselectRow(at: indexPath, animated: false)
return
}
selectedContacts.append(selectedContact)
if !multiSelectEnabled {
//Single selection code
resultSearchController.isActive = false
self.dismiss(animated: true) {
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
}
}
}
override open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return collation.section(forSectionIndexTitle: index)
}
override open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return collation.sectionIndexTitles
}
override open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let dataSource = resultSearchController.isActive ? filteredSections : sections
if dataSource[section].count > 0 {
return collation.sectionTitles[section]
} else {
return nil
}
}
// MARK: - Button Actions
func onTouchCancelButton() {
contactsPickerDelegate?.contactsPicker(self, didCancel: NSError(domain: "contactsPickerErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey: "User Canceled Selection"]))
dismiss(animated: true, completion: nil)
}
func onTouchDoneButton() {
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
dismiss(animated: true, completion: nil)
}
// MARK: - Search Actions
open func updateSearchResults(for searchController: UISearchController) {
if let searchText = resultSearchController.searchBar.text , searchController.isActive {
let predicate: NSPredicate
if searchText.characters.count == 0 {
filteredSections = sections
} else {
do {
predicate = CNContact.predicateForContacts(matchingName: searchText)
let filteredContacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: allowedContactKeys)
filteredSections = collatedContacts(filteredContacts)
} catch let error as NSError {
Logger.error("\(self.TAG) updating search results failed with error: \(error)")
}
}
self.tableView.reloadData()
}
}
open func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
@available(iOS 9.0, *)
let ContactSortOrder = computeSortOrder()
@available(iOS 9.0, *)
func computeSortOrder() -> CNContactSortOrder {
let comparator = CNContact.comparator(forNameSortOrder: .userDefault)
let contact0 = CNMutableContact()
contact0.givenName = "A"
contact0.familyName = "Z"
let contact1 = CNMutableContact()
contact1.givenName = "Z"
contact1.familyName = "A"
let result = comparator(contact0, contact1)
if result == .orderedAscending {
return .givenName
} else {
return .familyName
}
}
@available(iOS 9.0, *)
fileprivate extension CNContact {
/**
* Sorting Key used by collation
*/
@objc var nameForCollating: String {
get {
let compositeName: String
if ContactSortOrder == .familyName {
compositeName = "\(self.familyName) \(self.givenName)"
} else {
compositeName = "\(self.givenName) \(self.familyName)"
}
return compositeName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
}
}

View File

@ -7,8 +7,7 @@ import ContactsUI
import MessageUI
@objc(OWSInviteFlow)
class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate {
class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate, ContactsPickerDelegate {
enum Channel {
case message, mail, twitter
}
@ -20,11 +19,13 @@ class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControl
let actionSheetController: UIAlertController
let presentingViewController: UIViewController
let contactsManager: OWSContactsManager
var channel: Channel?
required init(presentingViewController: UIViewController) {
required init(presentingViewController: UIViewController, contactsManager: OWSContactsManager) {
self.presentingViewController = presentingViewController
self.contactsManager = contactsManager
actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
super.init()
@ -68,7 +69,7 @@ class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControl
let tweetUrl = URL(string: installUrl)
twitterViewController.add(tweetUrl)
twitterViewController.add(#imageLiteral(resourceName: "logo_with_background"))
twitterViewController.add(#imageLiteral(resourceName: "twitter_sharing_image"))
let tweetTitle = NSLocalizedString("SHARE_ACTION_TWEET", comment:"action sheet item")
return UIAlertAction(title: tweetTitle, style: .default) { action in
@ -82,24 +83,10 @@ class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControl
return UIAlertAction(title: NSLocalizedString("DISMISS_BUTTON_TEXT", comment:""), style: .cancel)
}
// MARK: ContactPickerDelegate
// MARK: ContactsPickerDelegate
/*!
* @abstract Invoked when the picker is closed.
* @discussion The picker will be dismissed automatically after a contact or property is picked.
*/
@available(iOS 9.0, *)
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
Logger.debug("\(TAG) pickerDidCancel")
}
/*!
* @abstract Plural delegate methods.
* @discussion These delegate methods will be invoked when the user is done selecting multiple contacts or properties.
* Implementing one of these methods will configure the picker for multi-selection.
*/
@available(iOS 9.0, *)
func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
Logger.debug("\(TAG) didSelectContacts:\(contacts)")
guard let inviteChannel = channel else {
@ -109,24 +96,38 @@ class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControl
switch inviteChannel {
case .message:
sendSMSTo(contacts: contacts)
let phoneNumbers: [String] = contacts.map { $0.userTextPhoneNumbers.first }.filter { $0 != nil }.map { $0! }
sendSMSTo(phoneNumbers: phoneNumbers)
case .mail:
sendMailTo(contacts:contacts)
let recipients: [String] = contacts.map { $0.emails.first }.filter { $0 != nil }.map { $0! }
sendMailTo(emails: recipients)
default:
Logger.error("\(TAG) unexpected channel after returning from contact picker: \(inviteChannel)")
}
}
@available(iOS 9.0, *)
func contactPicker(_ picker: CNContactPickerViewController, didSelectContactProperties contactProperties: [CNContactProperty]) {
Logger.debug("\(TAG) didSelectContactProperties:\(contactProperties)")
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
guard let inviteChannel = channel else {
Logger.error("\(TAG) unexpected nil channel in contact picker.")
return true
}
switch inviteChannel {
case .message:
return contact.userTextPhoneNumbers.count > 0
case .mail:
return contact.emails.count > 0
default:
Logger.error("\(TAG) unexpected channel after returning from contact picker: \(inviteChannel)")
}
return true
}
// MARK: SMS
@available(iOS 9.0, *)
func messageAction() -> UIAlertAction? {
guard MFMessageComposeViewController.canSendText() else {
Logger.info("\(TAG) Device cannot send text")
return nil
@ -136,18 +137,13 @@ class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControl
return UIAlertAction(title: messageTitle, style: .default) { action in
Logger.debug("\(self.TAG) Chose message.")
self.channel = .message
let picker = CNContactPickerViewController()
picker.predicateForSelectionOfContact = NSPredicate(value: false)
picker.predicateForEnablingContact = NSPredicate(format: "phoneNumbers.@count > 0")
picker.delegate = self
self.presentingViewController.present(picker, animated: true, completion: nil)
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .phoneNumber)
let navigationController = UINavigationController(rootViewController: picker)
self.presentingViewController.present(navigationController, animated: true)
}
}
@available(iOS 9.0, *)
func sendSMSTo(contacts: [CNContact]) {
func sendSMSTo(phoneNumbers: [String]) {
self.presentingViewController.dismiss(animated: true) {
if #available(iOS 10.0, *) {
// iOS10 message compose view doesn't respect some system appearence attributes.
@ -159,7 +155,7 @@ class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControl
}
let messageComposeViewController = MFMessageComposeViewController()
messageComposeViewController.messageComposeDelegate = self
messageComposeViewController.recipients = contacts.map { $0.phoneNumbers.first }.filter { $0 != nil }.map { $0!.value.stringValue }
messageComposeViewController.recipients = phoneNumbers
let inviteText = NSLocalizedString("SMS_INVITE_BODY", comment:"body sent to contacts when inviting to Install Signal")
messageComposeViewController.body = inviteText.appending(" \(self.installUrl)")
@ -200,22 +196,18 @@ class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControl
Logger.debug("\(self.TAG) Chose mail.")
self.channel = .mail
let picker = CNContactPickerViewController()
picker.predicateForSelectionOfContact = NSPredicate(value: false)
picker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
picker.delegate = self
self.presentingViewController.present(picker, animated: true)
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .email)
let navigationController = UINavigationController(rootViewController: picker)
self.presentingViewController.present(navigationController, animated: true)
}
}
@available(iOS 9.0, *)
func sendMailTo(contacts: [CNContact]) {
func sendMailTo(emails recipientEmails: [String]) {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = self
let recipients: [String] = contacts.map { $0.emailAddresses.first }.filter { $0 != nil }.map { $0!.value as String }
mailComposeViewController.setBccRecipients(recipients)
mailComposeViewController.setBccRecipients(recipientEmails)
let subject = NSLocalizedString("EMAIL_INVITE_SUBJECT", comment:"subject of email sent to contacts when inviting to install Signal")
let bodyFormat = NSLocalizedString("EMAIL_INVITE_BODY", comment:"body of email sent to contacts when inviting to install Signal. Embeds {{link to install Signal}} and {{link to WhisperSystems home page}}")

View File

@ -75,6 +75,9 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie
[super viewDidLoad];
[self.navigationController.navigationBar setTranslucent:NO];
self.tableView.estimatedRowHeight = (CGFloat)60.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.contacts = self.contactsManager.signalContacts;
self.searchResults = self.contacts;
[self initializeSearch];
@ -430,6 +433,10 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section == MessageComposeTableViewControllerSectionInvite) {
if (floor(NSFoundationVersionNumber) < NSFoundationVersionNumber_iOS_9_0) {
// Invite flow not supported on iOS8
return 0;
}
return 1;
} else {
if (self.searchController.active) {
@ -454,17 +461,14 @@ NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableVie
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 52.0f;
}
#pragma mark - Table View delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == MessageComposeTableViewControllerSectionInvite) {
void (^showInvite)() = ^{
OWSInviteFlow *inviteFlow = [[OWSInviteFlow alloc] initWithPresentingViewController:self];
OWSInviteFlow *inviteFlow =
[[OWSInviteFlow alloc] initWithPresentingViewController:self contactsManager:self.contactsManager];
[self presentViewController:inviteFlow.actionSheetController
animated:YES
completion:^{

View File

@ -57,10 +57,36 @@ typedef enum {
@interface SettingsTableViewController () <UIAlertViewDelegate>
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
@end
@implementation SettingsTableViewController
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
_contactsManager = [Environment getCurrent].contactsManager;
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (!self) {
return self;
}
_contactsManager = [Environment getCurrent].contactsManager;
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
@ -133,7 +159,9 @@ typedef enum {
case kGeneralSection: {
switch (indexPath.row) {
case kInviteRow: {
OWSInviteFlow *inviteFlow = [[OWSInviteFlow alloc] initWithPresentingViewController:self];
OWSInviteFlow *inviteFlow =
[[OWSInviteFlow alloc] initWithPresentingViewController:self
contactsManager:self.contactsManager];
[self presentViewController:inviteFlow.actionSheetController
animated:YES
completion:^{

View File

@ -0,0 +1,118 @@
// Originally based on EPContacts
//
// Created by Prabaharan Elangovan on 13/10/15.
// Copyright © 2015 Prabaharan Elangovan. All rights reserved.
//
// Modified for Signal by Michael Kirk on 11/25/2016
// Parts Copyright © 2016 Open Whisper Systems. All rights reserved.
import UIKit
@available(iOS 9.0, *)
class ContactCell: UITableViewCell {
static let nib = UINib(nibName:"ContactCell", bundle: nil)
@IBOutlet weak var contactTextLabel: UILabel!
@IBOutlet weak var contactDetailTextLabel: UILabel!
@IBOutlet weak var contactImageView: UIImageView!
@IBOutlet weak var contactContainerView: UIView!
var contact: Contact?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
selectionStyle = UITableViewCellSelectionStyle.none
contactContainerView.layer.masksToBounds = true
contactContainerView.layer.cornerRadius = contactContainerView.frame.size.width/2
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
}
override func prepareForReuse() {
accessoryType = .none
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
accessoryType = selected ? .checkmark : .none
}
func didChangePreferredContentSize() {
contactTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
contactDetailTextLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
}
func updateContactsinUI(_ contact: Contact, subtitleType: SubtitleCellValue, contactsManager: OWSContactsManager) {
self.contact = contact
if contactTextLabel != nil {
contactTextLabel.attributedText = contact.cnContact?.formattedFullName(font:contactTextLabel.font)
}
updateSubtitleBasedonType(subtitleType, contact: contact)
if contact.image == nil {
let contactIdForDeterminingBackgroundColor: String
if let signalId = contact.parsedPhoneNumbers.first?.toE164() {
contactIdForDeterminingBackgroundColor = signalId
} else {
contactIdForDeterminingBackgroundColor = contact.fullName
}
let avatarBuilder = OWSContactAvatarBuilder(contactId:contactIdForDeterminingBackgroundColor,
name:contact.fullName,
contactsManager:contactsManager)
self.contactImageView?.image = avatarBuilder.buildDefaultImage();
} else {
self.contactImageView?.image = contact.image
}
}
func updateSubtitleBasedonType(_ subtitleType: SubtitleCellValue , contact: Contact) {
switch subtitleType {
case SubtitleCellValue.phoneNumber:
if contact.userTextPhoneNumbers.count > 0 {
self.contactDetailTextLabel.text = "\(contact.userTextPhoneNumbers[0])"
} else {
self.contactDetailTextLabel.text = NSLocalizedString("CONTACT_PICKER_NO_PHONE_NUMBERS_AVAILABLE", comment: "table cell subtitle when contact card has no known phone number")
}
case SubtitleCellValue.email:
if contact.emails.count > 0 {
self.contactDetailTextLabel.text = "\(contact.emails[0])"
} else {
self.contactDetailTextLabel.text = NSLocalizedString("CONTACT_PICKER_NO_EMAILS_AVAILABLE", comment: "table cell subtitle when contact card has no email")
}
}
}
}
@available(iOS 9.0, *)
fileprivate extension CNContact {
/**
* Bold the sorting portion of the name. e.g. if we sort by family name, bold the family name.
*/
func formattedFullName(font: UIFont) -> NSAttributedString? {
let keyToHighlight = ContactSortOrder == .familyName ? CNContactFamilyNameKey : CNContactGivenNameKey
if let attributedName = CNContactFormatter.attributedString(from: self, style: .fullName, defaultAttributes: nil) {
let highlightedName = attributedName.mutableCopy() as! NSMutableAttributedString
highlightedName.enumerateAttributes(in: NSMakeRange(0, highlightedName.length), options: [], using: { (attrs, range, stop) in
if let property = attrs[CNContactPropertyAttribute] as? String, property == keyToHighlight {
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold)
let boldAttributes = [
NSFontAttributeName: UIFont(descriptor:boldDescriptor!, size: 0)
]
highlightedName.addAttributes(boldAttributes, range: range)
}
})
return highlightedName
}
return nil
}
}

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11542" systemVersion="15G1108" 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="11524"/>
<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="60" id="KGk-i7-Jjw" customClass="ContactCell" customModule="Signal" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="60"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="59"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bKn-WF-9hk">
<rect key="frame" x="0.0" y="0.0" width="320" height="59"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="59" id="z02-WI-o4t"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HUe-8f-NjY">
<rect key="frame" x="60" y="10" width="250" height="21"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tqx-Gt-ofy">
<rect key="frame" x="60" y="35" width="234" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<color key="textColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Bvt-tZ-NrJ">
<rect key="frame" x="10" y="10" width="40" height="40"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mxT-DB-6lI">
<rect key="frame" x="0.0" y="0.0" width="40" height="40"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="0.7725490196" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="40" id="8v6-UT-2rU"/>
<constraint firstAttribute="height" constant="40" id="pAq-lQ-W6t"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="Bvt-tZ-NrJ" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="1V9-eI-crl"/>
<constraint firstAttribute="trailing" secondItem="Tqx-Gt-ofy" secondAttribute="trailing" constant="26" id="36W-0D-Qv3"/>
<constraint firstItem="HUe-8f-NjY" firstAttribute="leading" secondItem="Bvt-tZ-NrJ" secondAttribute="trailing" constant="10" id="8Ws-h4-oJp"/>
<constraint firstItem="bKn-WF-9hk" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="NDc-ZH-7qQ"/>
<constraint firstItem="Tqx-Gt-ofy" firstAttribute="leading" secondItem="Bvt-tZ-NrJ" secondAttribute="trailing" constant="10" id="PDp-yF-F2b"/>
<constraint firstItem="HUe-8f-NjY" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="10" id="R7t-dd-hcs"/>
<constraint firstItem="Tqx-Gt-ofy" firstAttribute="top" secondItem="HUe-8f-NjY" secondAttribute="bottom" constant="4" id="aNx-bp-Uj5"/>
<constraint firstAttribute="trailing" secondItem="HUe-8f-NjY" secondAttribute="trailing" constant="10" id="cOY-df-aFi"/>
<constraint firstItem="Bvt-tZ-NrJ" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="10" id="m7q-1p-kjc"/>
<constraint firstAttribute="bottom" secondItem="Tqx-Gt-ofy" secondAttribute="bottom" constant="6" id="mEe-il-eMD"/>
<constraint firstAttribute="bottom" secondItem="bKn-WF-9hk" secondAttribute="bottom" id="oHg-Fe-wLe"/>
<constraint firstAttribute="trailing" secondItem="bKn-WF-9hk" secondAttribute="trailing" id="p0G-Qg-21x"/>
<constraint firstItem="bKn-WF-9hk" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="q4R-qf-Bvi"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="contactContainerView" destination="Bvt-tZ-NrJ" id="qqg-f5-Xol"/>
<outlet property="contactDetailTextLabel" destination="Tqx-Gt-ofy" id="Jlj-TK-UJl"/>
<outlet property="contactImageView" destination="mxT-DB-6lI" id="aYl-FS-HAU"/>
<outlet property="contactTextLabel" destination="HUe-8f-NjY" id="Smr-wZ-MHr"/>
</connections>
<point key="canvasLocation" x="245" y="321"/>
</tableViewCell>
</objects>
</document>

View File

@ -1,5 +1,5 @@
#!/bin/bash
TARGETS="Signal/src ../SignalServiceKit/src Pods/JSQMessagesViewController"
TARGETS="Signal/src Libraries/EPContactsPicker ../SignalServiceKit/src Pods/JSQMessagesViewController"
TMP="$(mktemp -d)"
STRINGFILE="Signal/translations/en.lproj/Localizable.strings"