Mail and Message invite flow
* Spruce up compose contact-picker - Fix random sorting for contacts missing first or last name - Add Avatar to contact picker - de-dupe contacts Better copy for INVALID_MESSAGE error. // FREEBIE
This commit is contained in:
parent
bed5250397
commit
06ca3c9290
|
@ -26,6 +26,7 @@
|
|||
453D28B71D32BA5F00D523F0 /* OWSDisplayedMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B61D32BA5F00D523F0 /* OWSDisplayedMessage.m */; };
|
||||
453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; };
|
||||
453D28BB1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; };
|
||||
45514DE21DDFA183003EFF90 /* InviteFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45514DE11DDFA183003EFF90 /* InviteFlow.swift */; };
|
||||
45666EC61D99483D008FE134 /* OWSAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */; };
|
||||
45666EC91D994C0D008FE134 /* OWSGroupAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC81D994C0D008FE134 /* OWSGroupAvatarBuilder.m */; };
|
||||
45666F561D9B2827008FE134 /* OWSScrubbingLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666F551D9B2827008FE134 /* OWSScrubbingLogFormatter.m */; };
|
||||
|
@ -565,6 +566,7 @@
|
|||
453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesBubblesSizeCalculator.h; sourceTree = "<group>"; };
|
||||
453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesBubblesSizeCalculator.m; sourceTree = "<group>"; };
|
||||
454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
45514DE11DDFA183003EFF90 /* InviteFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InviteFlow.swift; sourceTree = "<group>"; };
|
||||
45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAvatarBuilder.h; sourceTree = "<group>"; };
|
||||
45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAvatarBuilder.m; sourceTree = "<group>"; };
|
||||
45666EC71D994C0D008FE134 /* OWSGroupAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGroupAvatarBuilder.h; sourceTree = "<group>"; };
|
||||
|
@ -1824,6 +1826,7 @@
|
|||
76EB050C18170B33006006FC /* InCallViewController.m */,
|
||||
458E382F1D6682450094BD24 /* OWSQRCodeScanningViewController.h */,
|
||||
458E38301D6682450094BD24 /* OWSQRCodeScanningViewController.m */,
|
||||
45514DE11DDFA183003EFF90 /* InviteFlow.swift */,
|
||||
);
|
||||
name = "View Controllers";
|
||||
path = "view controllers";
|
||||
|
@ -2834,6 +2837,7 @@
|
|||
FCB11D931A12A4AA002F93FB /* FullImageViewController.m in Sources */,
|
||||
B60C16651988999D00E97A6C /* VersionMigrations.m in Sources */,
|
||||
B97940271832BD2400BD66CB /* UIUtil.m in Sources */,
|
||||
45514DE21DDFA183003EFF90 /* InviteFlow.swift in Sources */,
|
||||
4CE0E3771B954546007210CF /* TSAnimatedAdapter.m in Sources */,
|
||||
76EB05BE18170B33006006FC /* ConfirmPacket.m in Sources */,
|
||||
4531C9C41DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m in Sources */,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "logo_with_background.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "logo_with_background@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "logo_with_background@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Signal/Images.xcassets/logo_with_background.imageset/logo_with_background.png
vendored
Normal file
BIN
Signal/Images.xcassets/logo_with_background.imageset/logo_with_background.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
Signal/Images.xcassets/logo_with_background.imageset/logo_with_background@2x.png
vendored
Normal file
BIN
Signal/Images.xcassets/logo_with_background.imageset/logo_with_background@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
Signal/Images.xcassets/logo_with_background.imageset/logo_with_background@3x.png
vendored
Normal file
BIN
Signal/Images.xcassets/logo_with_background.imageset/logo_with_background@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -38,7 +38,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2.6.6.0</string>
|
||||
<string>2.6.6.1</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LOGS_EMAIL</key>
|
||||
|
|
|
@ -10,6 +10,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@interface OWSContactAvatarBuilder : OWSAvatarBuilder
|
||||
|
||||
- (instancetype)initWithContactId:(NSString *)contactId
|
||||
name:(NSString *)name
|
||||
contactsManager:(OWSContactsManager *)contactsManager;
|
||||
|
||||
- (instancetype)initWithThread:(TSContactThread *)thread contactsManager:(OWSContactsManager *)contactsManager;
|
||||
|
||||
@end
|
||||
|
|
|
@ -22,20 +22,28 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@implementation OWSContactAvatarBuilder
|
||||
|
||||
- (instancetype)initWithThread:(TSContactThread *)thread contactsManager:(OWSContactsManager *)contactsManager
|
||||
- (instancetype)initWithContactId:(NSString *)contactId
|
||||
name:(NSString *)name
|
||||
contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_signalId = thread.contactIdentifier;
|
||||
_contactName = thread.name;
|
||||
_signalId = contactId;
|
||||
_contactName = name;
|
||||
_contactsManager = contactsManager;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithThread:(TSContactThread *)thread contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
return [self initWithContactId:thread.contactIdentifier name:thread.name contactsManager:contactsManager];
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildSavedImage
|
||||
{
|
||||
return [self.contactsManager imageForPhoneIdentifier:self.signalId];
|
||||
|
@ -43,6 +51,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (UIImage *)buildDefaultImage
|
||||
{
|
||||
UIImage *cachedAvatar = [self.contactsManager.avatarCache objectForKey:self.signalId];
|
||||
if (cachedAvatar) {
|
||||
return cachedAvatar;
|
||||
}
|
||||
|
||||
NSMutableString *initials = [NSMutableString string];
|
||||
|
||||
if (self.contactName.length > 0) {
|
||||
|
@ -61,11 +74,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
UIColor *backgroundColor = [UIColor backgroundColorForContact:self.signalId];
|
||||
|
||||
return [[JSQMessagesAvatarImageFactory avatarImageWithUserInitials:initials
|
||||
backgroundColor:backgroundColor
|
||||
textColor:[UIColor whiteColor]
|
||||
font:[UIFont ows_boldFontWithSize:36.0]
|
||||
diameter:100] avatarImage];
|
||||
UIImage *image = [[JSQMessagesAvatarImageFactory avatarImageWithUserInitials:initials
|
||||
backgroundColor:backgroundColor
|
||||
textColor:[UIColor whiteColor]
|
||||
font:[UIFont ows_boldFontWithSize:36.0]
|
||||
diameter:100] avatarImage];
|
||||
|
||||
[self.contactsManager.avatarCache setObject:image forKey:self.signalId];
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#import "PropertyListPreferences.h"
|
||||
#import "PushManager.h"
|
||||
#import "RPAccountManager.h"
|
||||
#import "UIUtil.h"
|
||||
#import <SignalServiceKit/NSDate+millisecondTimeStamp.h>
|
||||
#import <SignalServiceKit/OWSEndSessionMessage.h>
|
||||
#import <SignalServiceKit/OWSError.h>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11542" systemVersion="16C41b" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="tuk-0x-yCb">
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11542" systemVersion="15G1108" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="tuk-0x-yCb">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
|
@ -1184,15 +1184,32 @@
|
|||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="9FD-M0-WLe" style="IBUITableViewCellStyleDefault" id="a7a-qe-87x" userLabel="Invite Your Friends">
|
||||
<rect key="frame" x="0.0" y="382" 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"/>
|
||||
<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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="FdD-MV-PUQ">
|
||||
<cells>
|
||||
<tableViewCell autoresizesSubviews="NO" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="100" id="R82-FT-SEA" userLabel="Destroy Account Cell">
|
||||
<rect key="frame" x="0.0" y="382" width="375" height="100"/>
|
||||
<rect key="frame" x="0.0" y="426" 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">
|
||||
|
@ -1240,6 +1257,7 @@
|
|||
<outlet property="aboutLabel" destination="qeN-f1-cIQ" id="kmc-iU-NK5"/>
|
||||
<outlet property="advancedLabel" destination="dkL-Nz-E6H" id="HUw-SB-apQ"/>
|
||||
<outlet property="destroyAccountButton" destination="4Mk-ly-6fq" id="6Xj-Rb-kfF"/>
|
||||
<outlet property="inviteLabel" destination="9FD-M0-WLe" id="TfU-b4-OtN"/>
|
||||
<outlet property="linkedDevicesLabel" destination="hrc-lZ-NeA" id="VkD-E5-2kW"/>
|
||||
<outlet property="networkStatusHeader" destination="uNq-FV-lwt" id="vca-cC-nXG"/>
|
||||
<outlet property="networkStatusLabel" destination="tg3-dQ-odw" id="l6J-8y-maW"/>
|
||||
|
@ -1376,25 +1394,36 @@
|
|||
<rect key="frame" x="0.0" y="22" width="375" height="48"/>
|
||||
<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="47.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="urb-Me-knG">
|
||||
<rect key="frame" x="20" y="4" width="275" height="39.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<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"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="11n-jp-FCg">
|
||||
<rect key="frame" x="16" y="4" width="39.5" height="39.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="11n-jp-FCg" secondAttribute="height" multiplier="1:1" id="CMp-Im-YMw"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottomMargin" secondItem="urb-Me-knG" secondAttribute="bottom" constant="-4" id="3fF-H3-Spz"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="urb-Me-knG" secondAttribute="trailing" constant="72" id="R9g-GE-nWU"/>
|
||||
<constraint firstItem="urb-Me-knG" firstAttribute="leading" secondItem="EqP-87-4hZ" secondAttribute="leadingMargin" constant="12" id="Zjd-vI-V9V"/>
|
||||
<constraint firstItem="urb-Me-knG" firstAttribute="top" secondItem="EqP-87-4hZ" secondAttribute="topMargin" constant="-4" id="iex-zH-iTD"/>
|
||||
<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="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>
|
||||
</tableViewCellContentView>
|
||||
<inset key="separatorInset" minX="15" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
<connections>
|
||||
<outlet property="nameLabel" destination="urb-Me-knG" id="2h5-l1-QDQ"/>
|
||||
<outlet property="avatarView" destination="11n-jp-FCg" id="OBx-gj-ljU"/>
|
||||
<outlet property="nameLabel" destination="4zF-SU-q4z" id="ita-p1-Vod"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
|
@ -1405,8 +1434,7 @@
|
|||
</tableView>
|
||||
<navigationItem key="navigationItem" title="New Message" id="D6A-aV-gEq">
|
||||
<barButtonItem key="backBarButtonItem" title=" " id="rvF-0w-DNd"/>
|
||||
<barButtonItem key="leftBarButtonItem" image="btnCancel--white" id="bP2-Tc-byJ">
|
||||
<inset key="imageInsets" minX="-10" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="stop" id="bP2-Tc-byJ">
|
||||
<color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<action selector="closeAction:" destination="Qga-41-bw2" id="i8h-aS-Y3o"/>
|
||||
|
@ -1420,8 +1448,29 @@
|
|||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="inviteCell" destination="Pgm-Qb-oPu" id="Nf5-sS-IKL"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="fUD-iU-Cax" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="ContactTableInviteCell" textLabel="QGl-1D-W9D" rowHeight="48" style="IBUITableViewCellStyleDefault" id="Pgm-Qb-oPu" userLabel="Invite Cell">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Pgm-Qb-oPu" id="amG-75-RNT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="342" height="47"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<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"/>
|
||||
<color key="textColor" red="0.09412795243864841" green="0.43645224658557435" blue="0.71380208333333339" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="tintColor" red="0.1135657504" green="0.4787300229" blue="0.89595204589999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<searchDisplayController id="f1M-Dk-nMv"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-2357" y="-235"/>
|
||||
|
|
|
@ -18,6 +18,7 @@ typedef void (^ABReloadRequestCompletionBlock)(NSArray *contacts);
|
|||
@interface OWSContactsManager : NSObject <ContactsManagerProtocol>
|
||||
|
||||
@property CNContactStore *contactStore;
|
||||
@property NSCache<NSString *, UIImage *> *avatarCache;
|
||||
|
||||
- (ObservableValue *)getObservableContacts;
|
||||
|
||||
|
|
|
@ -25,11 +25,15 @@ typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL *);
|
|||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_life = [TOCCancelTokenSource new];
|
||||
_observableContactsController = [ObservableValueController observableValueControllerWithInitialValue:nil];
|
||||
_latestContactsById = @{};
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_life = [TOCCancelTokenSource new];
|
||||
_observableContactsController = [ObservableValueController observableValueControllerWithInitialValue:nil];
|
||||
_latestContactsById = @{};
|
||||
_avatarCache = [NSCache new];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
@ -67,11 +71,17 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
|
|||
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context) {
|
||||
OWSContactsManager *contactsManager = (__bridge OWSContactsManager *)context;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[contactsManager pullLatestAddressBook];
|
||||
[contactsManager intersectContacts];
|
||||
[contactsManager handleAddressBookChanged];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleAddressBookChanged
|
||||
{
|
||||
[self pullLatestAddressBook];
|
||||
[self intersectContacts];
|
||||
[self.avatarCache removeAllObjects];
|
||||
}
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupAddressBook {
|
||||
|
@ -81,8 +91,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
|
|||
ABAddressBookRef cfAddressBook = (__bridge ABAddressBookRef)addressBook;
|
||||
ABAddressBookRegisterExternalChangeCallback(cfAddressBook, onAddressBookChanged, (__bridge void *)self);
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self pullLatestAddressBook];
|
||||
[self intersectContacts];
|
||||
[self handleAddressBookChanged];
|
||||
});
|
||||
}];
|
||||
});
|
||||
|
@ -370,9 +379,13 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
|
|||
#pragma mark - Whisper User Management
|
||||
|
||||
- (NSArray *)getSignalUsersFromContactsArray:(NSArray *)contacts {
|
||||
return [[contacts filter:^int(Contact *contact) {
|
||||
return [contact isSignalContact];
|
||||
}] sortedArrayUsingComparator:[[self class] contactComparator]];
|
||||
NSMutableDictionary *signalContacts = [NSMutableDictionary new];
|
||||
for (Contact *contact in contacts) {
|
||||
if ([contact isSignalContact]) {
|
||||
signalContacts[contact.textSecureIdentifiers.firstObject] = contact;
|
||||
}
|
||||
}
|
||||
return [signalContacts.allValues sortedArrayUsingComparator:[[self class] contactComparator]];
|
||||
}
|
||||
|
||||
+ (NSComparator)contactComparator {
|
||||
|
@ -383,9 +396,33 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in
|
|||
BOOL firstNameOrdering = ABPersonGetSortOrdering() == kABPersonCompositeNameFormatFirstNameFirst ? YES : NO;
|
||||
|
||||
if (firstNameOrdering) {
|
||||
return [contact1.firstName caseInsensitiveCompare:contact2.firstName];
|
||||
if (contact1.firstName) {
|
||||
if (contact2.firstName) {
|
||||
return [contact1.firstName caseInsensitiveCompare:contact2.firstName];
|
||||
} else {
|
||||
return [contact1.firstName caseInsensitiveCompare:contact2.lastName];
|
||||
}
|
||||
} else {
|
||||
if (contact2.firstName) {
|
||||
return [contact1.lastName caseInsensitiveCompare:contact2.firstName];
|
||||
} else {
|
||||
return [contact1.lastName caseInsensitiveCompare:contact2.lastName];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return [contact1.lastName caseInsensitiveCompare:contact2.lastName];
|
||||
if (contact1.lastName) {
|
||||
if (contact2.lastName) {
|
||||
return [contact1.lastName caseInsensitiveCompare:contact2.lastName];
|
||||
} else {
|
||||
return [contact1.lastName caseInsensitiveCompare:contact2.firstName];
|
||||
}
|
||||
} else {
|
||||
if (contact2.lastName) {
|
||||
return [contact1.firstName caseInsensitiveCompare:contact2.lastName];
|
||||
} else {
|
||||
return [contact1.firstName caseInsensitiveCompare:contact2.firstName];
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ typedef void (^completionBlock)(void);
|
|||
|
||||
@interface UIUtil : NSObject
|
||||
|
||||
+ (void)applyRoundedBorderToImageView:(UIImageView *__strong *)imageView;
|
||||
+ (void)applyRoundedBorderToImageView:(UIImageView *)imageView;
|
||||
+ (void)removeRoundedBorderToImageView:(UIImageView *__strong *)imageView;
|
||||
|
||||
+ (completionBlock)modalCompletionBlock;
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
|
||||
@implementation UIUtil
|
||||
|
||||
+ (void)applyRoundedBorderToImageView:(UIImageView *__strong *)imageView {
|
||||
[[*imageView layer] setBorderWidth:CONTACT_PICTURE_VIEW_BORDER_WIDTH];
|
||||
[[*imageView layer] setBorderColor:[[UIColor clearColor] CGColor]];
|
||||
[[*imageView layer] setCornerRadius:CGRectGetWidth([*imageView frame]) / 2];
|
||||
[[*imageView layer] setMasksToBounds:YES];
|
||||
+ (void)applyRoundedBorderToImageView:(UIImageView *)imageView
|
||||
{
|
||||
imageView.layer.borderWidth = CONTACT_PICTURE_VIEW_BORDER_WIDTH;
|
||||
imageView.layer.borderColor = [UIColor clearColor].CGColor;
|
||||
imageView.layer.cornerRadius = CGRectGetWidth(imageView.frame) / 2;
|
||||
imageView.layer.masksToBounds = YES;
|
||||
}
|
||||
|
||||
|
||||
+ (void)removeRoundedBorderToImageView:(UIImageView *__strong *)imageView {
|
||||
[[*imageView layer] setBorderWidth:0];
|
||||
[[*imageView layer] setCornerRadius:0];
|
||||
|
@ -42,7 +42,7 @@
|
|||
|
||||
[[UIBarButtonItem appearanceWhenContainedIn:[UISearchBar class], nil] setTintColor:[UIColor ows_materialBlueColor]];
|
||||
|
||||
|
||||
[[UISwitch appearance] setOnTintColor:[UIColor ows_materialBlueColor]];
|
||||
[[UIToolbar appearance] setTintColor:[UIColor ows_materialBlueColor]];
|
||||
[[UIBarButtonItem appearance] setTintColor:[UIColor whiteColor]];
|
||||
|
||||
|
@ -53,9 +53,6 @@
|
|||
NSForegroundColorAttributeName : [UIColor whiteColor],
|
||||
NSShadowAttributeName : shadow,
|
||||
};
|
||||
|
||||
[[UISwitch appearance] setOnTintColor:[UIColor ows_materialBlueColor]];
|
||||
|
||||
[[UINavigationBar appearance] setTitleTextAttributes:navbarTitleTextAttributes];
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,11 @@
|
|||
|
||||
@end
|
||||
|
||||
typedef NS_ENUM(NSUInteger, AboutTableViewControllerSection) {
|
||||
AboutTableViewControllerSectionInformation,
|
||||
AboutTableViewControllerSectionHelp
|
||||
};
|
||||
|
||||
@implementation AboutTableViewController
|
||||
|
||||
- (instancetype)init {
|
||||
|
@ -64,32 +69,19 @@
|
|||
self.footerView.font = [UIFont ows_regularFontWithSize:15.0f];
|
||||
self.footerView.numberOfLines = 2;
|
||||
self.footerView.textAlignment = NSTextAlignmentCenter;
|
||||
|
||||
|
||||
// Twitter Invite
|
||||
self.twitterInviteCell = [[UITableViewCell alloc] init];
|
||||
self.twitterInviteCell.textLabel.text = NSLocalizedString(@"SETTINGS_SHARE_INSTALL", @"");
|
||||
|
||||
UIImageView *twitterImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"twitter_logo"]];
|
||||
[twitterImageView setFrame:CGRectMake(0, 0, 34, 34)];
|
||||
twitterImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
|
||||
self.twitterInviteCell.accessoryView = twitterImageView;
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 3;
|
||||
return 2;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
switch (section) {
|
||||
case 0:
|
||||
case AboutTableViewControllerSectionInformation:
|
||||
return 1;
|
||||
case 1:
|
||||
return 1;
|
||||
case 2:
|
||||
case AboutTableViewControllerSectionHelp:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
|
@ -98,13 +90,10 @@
|
|||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
switch (section) {
|
||||
case 0:
|
||||
case AboutTableViewControllerSectionInformation:
|
||||
return NSLocalizedString(@"SETTINGS_INFORMATION_HEADER", @"");
|
||||
case 1:
|
||||
return NSLocalizedString(@"SETTINGS_INVITE_HEADER", @"");
|
||||
case 2:
|
||||
case AboutTableViewControllerSectionHelp:
|
||||
return NSLocalizedString(@"SETTINGS_HELP_HEADER", @"");
|
||||
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
|
@ -112,11 +101,9 @@
|
|||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
switch (indexPath.section) {
|
||||
case 0:
|
||||
case AboutTableViewControllerSectionInformation:
|
||||
return self.versionCell;
|
||||
case 1:
|
||||
return self.twitterInviteCell;
|
||||
case 2:
|
||||
case AboutTableViewControllerSectionHelp:
|
||||
return self.supportCell;
|
||||
}
|
||||
|
||||
|
@ -127,10 +114,7 @@
|
|||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
|
||||
switch (indexPath.section) {
|
||||
case 1:
|
||||
[self tappedInviteTwitter];
|
||||
break;
|
||||
case 2:
|
||||
case AboutTableViewControllerSectionHelp:
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://support.whispersystems.org"]];
|
||||
break;
|
||||
|
||||
|
@ -140,25 +124,11 @@
|
|||
}
|
||||
|
||||
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
|
||||
return section == 2 ? self.footerView : nil;
|
||||
return section == AboutTableViewControllerSectionHelp ? self.footerView : nil;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
|
||||
return section == 2 ? 60.0f : 0;
|
||||
}
|
||||
|
||||
- (void)tappedInviteTwitter {
|
||||
if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) {
|
||||
SLComposeViewController *tweetSheet =
|
||||
[SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
|
||||
|
||||
NSString *tweetString = [NSString stringWithFormat:NSLocalizedString(@"SETTINGS_INVITE_TWITTER_TEXT", @"")];
|
||||
[tweetSheet setInitialText:tweetString];
|
||||
[tweetSheet addURL:[NSURL URLWithString:@"https://whispersystems.org/signal/install/"]];
|
||||
tweetSheet.completionHandler = ^(SLComposeViewControllerResult result) {
|
||||
};
|
||||
[self presentViewController:tweetSheet animated:YES completion:[UIUtil modalCompletionBlock]];
|
||||
}
|
||||
return section == AboutTableViewControllerSectionHelp ? 60.0f : 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
|
||||
if (_potentiallyKnownContact) {
|
||||
if (_potentiallyKnownContact.image) {
|
||||
[UIUtil applyRoundedBorderToImageView:&_contactImageView];
|
||||
[UIUtil applyRoundedBorderToImageView:_contactImageView];
|
||||
}
|
||||
|
||||
_nameLabel.text = _potentiallyKnownContact.fullName;
|
||||
|
|
|
@ -70,7 +70,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
self.snippetLabel.text = snippetLabel;
|
||||
self.timeLabel.attributedText = attributedDate;
|
||||
self.contactPictureView.image = avatar;
|
||||
[UIUtil applyRoundedBorderToImageView:&_contactPictureView];
|
||||
[UIUtil applyRoundedBorderToImageView:_contactPictureView];
|
||||
|
||||
self.separatorInset = UIEdgeInsetsMake(0, _contactPictureView.frame.size.width * 1.5f, 0, 0);
|
||||
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
// Created by Michael Kirk on 11/18/16.
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Social
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
|
||||
@objc(OWSInviteFlow)
|
||||
class InviteFlow: NSObject, CNContactPickerDelegate, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate {
|
||||
|
||||
enum Channel {
|
||||
case message, mail, twitter
|
||||
}
|
||||
|
||||
let TAG = "[ShareActions]"
|
||||
|
||||
// redirects to either the ios appstore or google play store depending on the user agent.
|
||||
// Appropriate to send to mobile browsers, but for email we want to expose both directs link
|
||||
// since an e.g. mac user, might have an Android, or Windows user might have an iPhone.
|
||||
let mobileInstallUrl = "https://signal.org/install/"
|
||||
let iOSInstallUrl = "https://itunes.apple.com/us/app/signal-private-messenger/id874139669"
|
||||
let androidInstallUrl = "https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms"
|
||||
let homepageUrl = "https://whispersystems.org"
|
||||
|
||||
let actionSheetController: UIAlertController
|
||||
let presentingViewController: UIViewController
|
||||
var channel: Channel?
|
||||
|
||||
required init(presentingViewController: UIViewController) {
|
||||
self.presentingViewController = presentingViewController
|
||||
|
||||
actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
super.init()
|
||||
|
||||
actionSheetController.addAction(dismissAction())
|
||||
|
||||
if #available(iOS 9.0, *) {
|
||||
if let messageAction = messageAction() {
|
||||
actionSheetController.addAction(messageAction)
|
||||
}
|
||||
|
||||
if let mailAction = mailAction() {
|
||||
actionSheetController.addAction(mailAction)
|
||||
}
|
||||
}
|
||||
|
||||
if let tweetAction = tweetAction() {
|
||||
actionSheetController.addAction(tweetAction)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Twitter
|
||||
|
||||
func canTweet() -> Bool {
|
||||
return SLComposeViewController.isAvailable(forServiceType: SLServiceTypeTwitter)
|
||||
}
|
||||
|
||||
func tweetAction() -> UIAlertAction? {
|
||||
guard canTweet() else {
|
||||
Logger.info("\(TAG) Twitter not supported.")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let twitterViewController = SLComposeViewController(forServiceType: SLServiceTypeTwitter) else {
|
||||
Logger.error("\(TAG) unable to build twitter controller.")
|
||||
return nil
|
||||
}
|
||||
|
||||
let tweetString = NSLocalizedString("SETTINGS_INVITE_TWITTER_TEXT", comment:"content of tweet when inviting via twitter")
|
||||
twitterViewController.setInitialText(tweetString)
|
||||
|
||||
let tweetUrl = URL(string: mobileInstallUrl)
|
||||
twitterViewController.add(tweetUrl)
|
||||
twitterViewController.add(#imageLiteral(resourceName: "logo_with_background"))
|
||||
|
||||
let tweetTitle = NSLocalizedString("SHARE_ACTION_TWEET", comment:"action sheet item")
|
||||
return UIAlertAction(title: tweetTitle, style: .default) { action in
|
||||
Logger.debug("\(self.TAG) Chose tweet")
|
||||
|
||||
self.presentingViewController.present(twitterViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissAction() -> UIAlertAction {
|
||||
return UIAlertAction(title: NSLocalizedString("DISMISS_BUTTON_TEXT", comment:""), style: .cancel)
|
||||
}
|
||||
|
||||
// MARK: ContactPickerDelegate
|
||||
|
||||
/*!
|
||||
* @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]) {
|
||||
Logger.debug("\(TAG) didSelectContacts:\(contacts)")
|
||||
|
||||
guard let inviteChannel = channel else {
|
||||
Logger.error("\(TAG) unexpected nil channel after returning from contact picker.")
|
||||
return
|
||||
}
|
||||
|
||||
switch inviteChannel {
|
||||
case .message:
|
||||
sendSMSTo(contacts: contacts)
|
||||
case .mail:
|
||||
sendMailTo(contacts:contacts)
|
||||
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)")
|
||||
}
|
||||
|
||||
// MARK: SMS
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
func messageAction() -> UIAlertAction? {
|
||||
|
||||
guard MFMessageComposeViewController.canSendText() else {
|
||||
Logger.info("\(TAG) Device cannot send text")
|
||||
return nil
|
||||
}
|
||||
|
||||
let messageTitle = NSLocalizedString("SHARE_ACTION_MESSAGE", comment: "action sheet item to open native messages app")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
func sendSMSTo(contacts: [CNContact]) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
if #available(iOS 10.0, *) {
|
||||
// iOS10 message compose view doesn't respect some system appearence attributes.
|
||||
// Specifically, the title is white, but the navbar is gray.
|
||||
// So, we have to set system appearence before init'ing the message compose view controller in order
|
||||
// to make its colors legible.
|
||||
// Then we have to be sure to set it back in the ComposeViewControllerDelegate callback.
|
||||
UIUtil.applyDefaultSystemAppearence()
|
||||
}
|
||||
let messageComposeViewController = MFMessageComposeViewController()
|
||||
messageComposeViewController.messageComposeDelegate = self
|
||||
messageComposeViewController.recipients = contacts.map { $0.phoneNumbers.first }.filter { $0 != nil }.map { $0!.value.stringValue }
|
||||
|
||||
let inviteText = NSLocalizedString("SMS_INVITE_BODY", comment:"body sent to contacts when inviting to Install Signal")
|
||||
messageComposeViewController.body = inviteText.appending(" \(self.mobileInstallUrl)")
|
||||
self.presentingViewController.navigationController?.present(messageComposeViewController, animated:true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: MessageComposeViewControllerDelegate
|
||||
|
||||
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
|
||||
// Revert system styling applied to make messaging app legible on iOS10.
|
||||
UIUtil.applySignalAppearence()
|
||||
self.presentingViewController.dismiss(animated: true, completion: nil)
|
||||
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment:"Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: NSLocalizedString("DISMISS_BUTTON_TEXT", comment:""), style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("\(self.TAG) user successfully invited their friends via SMS.")
|
||||
case .cancelled:
|
||||
Logger.debug("\(self.TAG) user cancelled message invite")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Mail
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
func mailAction() -> UIAlertAction? {
|
||||
guard MFMailComposeViewController.canSendMail() else {
|
||||
Logger.info("\(TAG) Device cannot send mail")
|
||||
return nil
|
||||
}
|
||||
|
||||
let mailActionTitle = NSLocalizedString("SHARE_ACTION_MAIL", comment: "action sheet item to open native mail app")
|
||||
return UIAlertAction(title: mailActionTitle, style: .default) { action in
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
func sendMailTo(contacts: [CNContact]) {
|
||||
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)
|
||||
|
||||
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-iOS}}, {{link to install Signal-Android}}, and {{link to WhisperSystems home page}}")
|
||||
let body = String.init(format: bodyFormat, iOSInstallUrl, androidInstallUrl, homepageUrl)
|
||||
mailComposeViewController.setSubject(subject)
|
||||
mailComposeViewController.setMessageBody(body, isHTML: false)
|
||||
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
self.presentingViewController.navigationController?.present(mailComposeViewController, animated:true) {
|
||||
UIUtil.applySignalAppearence();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: MailComposeViewControllerDelegate
|
||||
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
self.presentingViewController.dismiss(animated: true, completion: nil)
|
||||
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment:"Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: NSLocalizedString("DISMISS_BUTTON_TEXT", comment:""), style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("\(self.TAG) user successfully invited their friends via mail.")
|
||||
case .saved:
|
||||
Logger.debug("\(self.TAG) user saved mail invite.")
|
||||
case .cancelled:
|
||||
Logger.debug("\(self.TAG) user cancelled mail invite.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,10 @@
|
|||
#import "Contact.h"
|
||||
#import "LocalizableText.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MessageComposeTableViewController : UITableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -12,15 +12,20 @@
|
|||
|
||||
#import "ContactTableViewCell.h"
|
||||
#import "ContactsUpdater.h"
|
||||
#import "OWSContactsSearcher.h"
|
||||
#import "Environment.h"
|
||||
#import "OWSContactsSearcher.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIUtil.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MessageComposeTableViewController () <UISearchBarDelegate,
|
||||
UISearchResultsUpdating,
|
||||
MFMessageComposeViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) IBOutlet UITableViewCell *inviteCell;
|
||||
|
||||
@property (nonatomic) UIButton *sendTextButton;
|
||||
@property (nonatomic, strong) UISearchController *searchController;
|
||||
@property (nonatomic, strong) UIActivityIndicatorView *activityIndicator;
|
||||
|
@ -30,21 +35,54 @@
|
|||
@property (nonatomic) NSString *currentSearchTerm;
|
||||
@property (copy) NSArray<Contact *> *contacts;
|
||||
@property (copy) NSArray<Contact *> *searchResults;
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
NSInteger const MessageComposeTableViewControllerSectionInvite = 0;
|
||||
NSInteger const MessageComposeTableViewControllerSectionContacts = 1;
|
||||
|
||||
NSString *const MessageComposeTableViewControllerCellInvite = @"ContactTableInviteCell";
|
||||
NSString *const MessageComposeTableViewControllerCellContact = @"ContactTableViewCell";
|
||||
|
||||
@implementation MessageComposeTableViewController
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_contactsManager = [Environment getCurrent].contactsManager;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_contactsManager = [Environment getCurrent].contactsManager;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
[self.navigationController.navigationBar setTranslucent:NO];
|
||||
|
||||
self.contacts = [[[Environment getCurrent] contactsManager] signalContacts];
|
||||
self.contacts = self.contactsManager.signalContacts;
|
||||
self.searchResults = self.contacts;
|
||||
[self initializeSearch];
|
||||
|
||||
self.searchController.searchBar.hidden = NO;
|
||||
self.searchController.searchBar.backgroundColor = [UIColor whiteColor];
|
||||
self.inviteCell.textLabel.text = NSLocalizedString(
|
||||
@"INVITE_FRIENDS_CONTACT_TABLE_BUTTON", @"Text for button at the top of the contact picker");
|
||||
|
||||
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
[self createLoadingAndBackgroundViews];
|
||||
|
@ -244,7 +282,7 @@
|
|||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
|
||||
NSString *searchString = [self.searchController.searchBar text];
|
||||
|
||||
[self filterContentForSearchText:searchString scope:nil];
|
||||
[self filterContentForSearchText:searchString];
|
||||
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
@ -263,7 +301,8 @@
|
|||
|
||||
#pragma mark - Filter
|
||||
|
||||
- (void)filterContentForSearchText:(NSString *)searchText scope:(NSString *)scope {
|
||||
- (void)filterContentForSearchText:(NSString *)searchText
|
||||
{
|
||||
OWSContactsSearcher *contactsSearcher = [[OWSContactsSearcher alloc] initWithContacts: self.contacts];
|
||||
self.searchResults = [contactsSearcher filterWithString:searchText];
|
||||
|
||||
|
@ -355,7 +394,7 @@
|
|||
case MessageComposeResultFailed: {
|
||||
UIAlertView *warningAlert =
|
||||
[[UIAlertView alloc] initWithTitle:@""
|
||||
message:NSLocalizedString(@"SEND_SMS_INVITE_FAILURE", @"")
|
||||
message:NSLocalizedString(@"SEND_INVITE_FAILURE", @"")
|
||||
delegate:nil
|
||||
cancelButtonTitle:NSLocalizedString(@"OK", @"")
|
||||
otherButtonTitles:nil];
|
||||
|
@ -369,7 +408,7 @@
|
|||
}];
|
||||
UIAlertView *successAlert =
|
||||
[[UIAlertView alloc] initWithTitle:@""
|
||||
message:NSLocalizedString(@"SEND_SMS_INVITE_SUCCESS", @"")
|
||||
message:NSLocalizedString(@"SEND_INVITE_SUCCESS", @"Alert body after invite succeeded")
|
||||
delegate:nil
|
||||
cancelButtonTitle:NSLocalizedString(@"OK", @"")
|
||||
otherButtonTitles:nil];
|
||||
|
@ -386,34 +425,33 @@
|
|||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
if (self.searchController.active) {
|
||||
return (NSInteger)[self.searchResults count];
|
||||
if (section == MessageComposeTableViewControllerSectionInvite) {
|
||||
return 1;
|
||||
} else {
|
||||
return (NSInteger)[self.contacts count];
|
||||
if (self.searchController.active) {
|
||||
return (NSInteger)[self.searchResults count];
|
||||
} else {
|
||||
return (NSInteger)[self.contacts count];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
if (indexPath.section == MessageComposeTableViewControllerSectionInvite) {
|
||||
return self.inviteCell;
|
||||
} else {
|
||||
ContactTableViewCell *cell = (ContactTableViewCell *)[tableView
|
||||
dequeueReusableCellWithIdentifier:MessageComposeTableViewControllerCellContact];
|
||||
|
||||
- (ContactTableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
ContactTableViewCell *cell =
|
||||
(ContactTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"ContactTableViewCell"];
|
||||
[cell configureWithContact:[self contactForIndexPath:indexPath] contactsManager:self.contactsManager];
|
||||
|
||||
if (cell == nil) {
|
||||
cell = [[ContactTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
|
||||
reuseIdentifier:@"ContactTableViewCell"];
|
||||
return cell;
|
||||
}
|
||||
|
||||
cell.shouldShowContactButtons = NO;
|
||||
|
||||
[cell configureWithContact:[self contactForIndexPath:indexPath]];
|
||||
|
||||
tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
@ -423,17 +461,31 @@
|
|||
#pragma mark - Table View delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *identifier = [[[self contactForIndexPath:indexPath] textSecureIdentifiers] firstObject];
|
||||
|
||||
[self dismissViewControllerAnimated:YES
|
||||
completion:^() {
|
||||
[Environment messageIdentifier:identifier withCompose:YES];
|
||||
if (indexPath.section == MessageComposeTableViewControllerSectionInvite) {
|
||||
void (^showInvite)() = ^{
|
||||
OWSInviteFlow *inviteFlow = [[OWSInviteFlow alloc] initWithPresentingViewController:self];
|
||||
[self presentViewController:inviteFlow.actionSheetController
|
||||
animated:YES
|
||||
completion:^{
|
||||
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}];
|
||||
}
|
||||
};
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
ContactTableViewCell *cell = (ContactTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
|
||||
cell.accessoryType = UITableViewCellAccessoryNone;
|
||||
if (self.presentedViewController) {
|
||||
// If search controller is active, dismiss it first.
|
||||
[self dismissViewControllerAnimated:YES completion:showInvite];
|
||||
} else {
|
||||
showInvite();
|
||||
}
|
||||
} else {
|
||||
NSString *identifier = [[[self contactForIndexPath:indexPath] textSecureIdentifiers] firstObject];
|
||||
|
||||
[self dismissViewControllerAnimated:YES
|
||||
completion:^() {
|
||||
[Environment messageIdentifier:identifier withCompose:YES];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (Contact *)contactForIndexPath:(NSIndexPath *)indexPath {
|
||||
|
@ -462,15 +514,14 @@
|
|||
}
|
||||
|
||||
- (void)refreshContacts {
|
||||
[[ContactsUpdater sharedUpdater]
|
||||
updateSignalContactIntersectionWithABContacts:[Environment getCurrent].contactsManager.allContacts
|
||||
[[ContactsUpdater sharedUpdater] updateSignalContactIntersectionWithABContacts:self.contactsManager.allContacts
|
||||
success:^{
|
||||
self.contacts = [[[Environment getCurrent] contactsManager] signalContacts];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateSearchResultsForSearchController:self.searchController];
|
||||
[self.tableView reloadData];
|
||||
[self updateAfterRefreshTry];
|
||||
});
|
||||
self.contacts = self.contactsManager.signalContacts;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateSearchResultsForSearchController:self.searchController];
|
||||
[self.tableView reloadData];
|
||||
[self updateAfterRefreshTry];
|
||||
});
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
@ -492,8 +543,8 @@
|
|||
|
||||
#pragma mark - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
||||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(nullable id)sender
|
||||
{
|
||||
self.searchController.active = NO;
|
||||
}
|
||||
|
||||
|
@ -510,3 +561,5 @@
|
|||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
@property (strong, nonatomic) IBOutlet UILabel *linkedDevicesLabel;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *advancedLabel;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *aboutLabel;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *inviteLabel;
|
||||
@property (strong, nonatomic) IBOutlet UIButton *destroyAccountButton;
|
||||
|
||||
- (IBAction)unregisterUser:(id)sender;
|
||||
|
|
|
@ -17,13 +17,13 @@
|
|||
|
||||
#import "TSSocketManager.h"
|
||||
|
||||
#import "OWSContactsManager.h"
|
||||
|
||||
#import "AboutTableViewController.h"
|
||||
#import "AdvancedSettingsTableViewController.h"
|
||||
#import "NotificationSettingsViewController.h"
|
||||
#import "OWSContactsManager.h"
|
||||
#import "PrivacySettingsTableViewController.h"
|
||||
#import "PushManager.h"
|
||||
#import "Signal-Swift.h"
|
||||
|
||||
#define kProfileCellHeight 87.0f
|
||||
#define kStandardCellHeight 44.0f
|
||||
|
@ -35,12 +35,13 @@
|
|||
#define kNotificationRow 1
|
||||
#define kAdvancedRow 3
|
||||
#define kAboutRow 4
|
||||
#define kInviteRow 5
|
||||
#define kNetworkRow 0
|
||||
#define kUnregisterRow 0
|
||||
|
||||
typedef enum {
|
||||
kRegisteredRows = 1,
|
||||
kGeneralRows = 5,
|
||||
kGeneralRows = 6,
|
||||
kNetworkStatusRows = 1,
|
||||
kUnregisterRows = 1,
|
||||
} kRowsForSection;
|
||||
|
@ -81,6 +82,8 @@ typedef enum {
|
|||
self.notificationsLabel.text = NSLocalizedString(@"SETTINGS_NOTIFICATIONS", nil);
|
||||
self.linkedDevicesLabel.text
|
||||
= NSLocalizedString(@"LINKED_DEVICES_TITLE", @"Menu item and navbar title for the device manager");
|
||||
self.inviteLabel.text = NSLocalizedString(@"SETTINGS_INVITE_TITLE", @"Settings table view cell label");
|
||||
|
||||
[self.destroyAccountButton setTitle:NSLocalizedString(@"SETTINGS_DELETE_ACCOUNT_BUTTON", @"")
|
||||
forState:UIControlStateNormal];
|
||||
}
|
||||
|
@ -153,7 +156,16 @@ typedef enum {
|
|||
[self.navigationController pushViewController:vc animated:YES];
|
||||
break;
|
||||
}
|
||||
case kInviteRow: {
|
||||
OWSInviteFlow *inviteFlow = [[OWSInviteFlow alloc] initWithPresentingViewController:self];
|
||||
[self presentViewController:inviteFlow.actionSheetController
|
||||
animated:YES
|
||||
completion:^{
|
||||
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}];
|
||||
}
|
||||
default:
|
||||
DDLogError(@"%@ Unhandled row selected at index path: %@", self.tag, indexPath);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -260,4 +272,16 @@ typedef enum {
|
|||
self.networkStatusLabel.textColor = [UIColor ows_yellowColor];
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -7,11 +7,14 @@
|
|||
*
|
||||
*/
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSContactsManager;
|
||||
|
||||
@interface ContactTableViewCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong) IBOutlet UILabel *nameLabel;
|
||||
@property BOOL shouldShowContactButtons;
|
||||
|
||||
- (void)configureWithContact:(Contact *)contact;
|
||||
- (void)configureWithContact:(Contact *)contact contactsManager:(OWSContactsManager *)contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
#import "ContactTableViewCell.h"
|
||||
#import "UIUtil.h"
|
||||
|
||||
#import "Environment.h"
|
||||
#import "OWSContactAvatarBuilder.h"
|
||||
#import "OWSContactsManager.h"
|
||||
#import "PhoneManager.h"
|
||||
#import "UIUtil.h"
|
||||
|
||||
@interface ContactTableViewCell ()
|
||||
|
||||
@property (strong, nonatomic) Contact *associatedContact;
|
||||
@property (nonatomic) IBOutlet UILabel *nameLabel;
|
||||
@property (nonatomic) IBOutlet UIImageView *avatarView;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -21,9 +23,19 @@
|
|||
return NSStringFromClass(self.class);
|
||||
}
|
||||
|
||||
- (void)configureWithContact:(Contact *)contact {
|
||||
self.associatedContact = contact;
|
||||
- (void)configureWithContact:(Contact *)contact contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
self.nameLabel.attributedText = [self attributedStringForContact:contact];
|
||||
self.avatarView.image =
|
||||
[[[OWSContactAvatarBuilder alloc] initWithContactId:contact.textSecureIdentifiers.firstObject
|
||||
name:contact.fullName
|
||||
contactsManager:contactsManager] build];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
[UIUtil applyRoundedBorderToImageView:self.avatarView];
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedStringForContact:(Contact *)contact {
|
||||
|
@ -34,11 +46,11 @@
|
|||
UIFont *lastNameFont;
|
||||
|
||||
if (ABPersonGetSortOrdering() == kABPersonCompositeNameFormatFirstNameFirst) {
|
||||
firstNameFont = [UIFont ows_mediumFontWithSize:_nameLabel.font.pointSize];
|
||||
lastNameFont = [UIFont ows_regularFontWithSize:_nameLabel.font.pointSize];
|
||||
firstNameFont = [UIFont ows_mediumFontWithSize:self.nameLabel.font.pointSize];
|
||||
lastNameFont = [UIFont ows_regularFontWithSize:self.nameLabel.font.pointSize];
|
||||
} else {
|
||||
firstNameFont = [UIFont ows_regularFontWithSize:_nameLabel.font.pointSize];
|
||||
lastNameFont = [UIFont ows_mediumFontWithSize:_nameLabel.font.pointSize];
|
||||
firstNameFont = [UIFont ows_regularFontWithSize:self.nameLabel.font.pointSize];
|
||||
lastNameFont = [UIFont ows_mediumFontWithSize:self.nameLabel.font.pointSize];
|
||||
}
|
||||
[fullNameAttributedString addAttribute:NSFontAttributeName
|
||||
value:firstNameFont
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue