diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 1b858cb17..514ec6b99 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 3400C7931EAF89CD008A8584 /* SendExternalFileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3400C7911EAF89CD008A8584 /* SendExternalFileViewController.m */; }; + 3400C7961EAF99F4008A8584 /* SelectThreadViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3400C7951EAF99F4008A8584 /* SelectThreadViewController.m */; }; + 3400C7991EAFB772008A8584 /* ThreadViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 3400C7981EAFB772008A8584 /* ThreadViewHelper.m */; }; 341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */; }; 34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; }; 34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; }; @@ -344,6 +347,12 @@ /* Begin PBXFileReference section */ 1B5E7D6C9007F5E5761D79DD /* libPods-SignalTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SignalTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3400C7901EAF89CD008A8584 /* SendExternalFileViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SendExternalFileViewController.h; sourceTree = ""; }; + 3400C7911EAF89CD008A8584 /* SendExternalFileViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SendExternalFileViewController.m; sourceTree = ""; }; + 3400C7941EAF99F4008A8584 /* SelectThreadViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SelectThreadViewController.h; sourceTree = ""; }; + 3400C7951EAF99F4008A8584 /* SelectThreadViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SelectThreadViewController.m; sourceTree = ""; }; + 3400C7971EAFB772008A8584 /* ThreadViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadViewHelper.h; sourceTree = ""; }; + 3400C7981EAFB772008A8584 /* ThreadViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadViewHelper.m; sourceTree = ""; }; 341BB7471DB727EE001E2975 /* JSQMediaItem+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMediaItem+OWS.h"; sourceTree = ""; }; 341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMediaItem+OWS.m"; sourceTree = ""; }; 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "fontawesome-webfont.ttf"; sourceTree = ""; }; @@ -894,6 +903,10 @@ 34B3F8651E8DF1700035BE1A /* PrivacySettingsTableViewController.m */, 34B3F8661E8DF1700035BE1A /* RegistrationViewController.h */, 34B3F8671E8DF1700035BE1A /* RegistrationViewController.m */, + 3400C7941EAF99F4008A8584 /* SelectThreadViewController.h */, + 3400C7951EAF99F4008A8584 /* SelectThreadViewController.m */, + 3400C7901EAF89CD008A8584 /* SendExternalFileViewController.h */, + 3400C7911EAF89CD008A8584 /* SendExternalFileViewController.m */, 34B3F8681E8DF1700035BE1A /* SettingsTableViewController.h */, 34B3F8691E8DF1700035BE1A /* SettingsTableViewController.m */, 34B3F86A1E8DF1700035BE1A /* ShowGroupMembersViewController.h */, @@ -903,6 +916,8 @@ 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */, 34B3F86F1E8DF1700035BE1A /* SignalsViewController.h */, 34B3F8701E8DF1700035BE1A /* SignalsViewController.m */, + 3400C7971EAFB772008A8584 /* ThreadViewHelper.h */, + 3400C7981EAFB772008A8584 /* ThreadViewHelper.m */, 34B3F8A01E8EA6040035BE1A /* ViewControllerUtils.h */, 34B3F8A11E8EA6040035BE1A /* ViewControllerUtils.m */, ); @@ -1988,6 +2003,7 @@ B62D53F71A23CCAD009AAF82 /* TSMessageAdapter.m in Sources */, 76EB063C18170B33006006FC /* NumberUtil.m in Sources */, B6A3EB4B1A423B3800B2236B /* TSPhotoAdapter.m in Sources */, + 3400C7961EAF99F4008A8584 /* SelectThreadViewController.m in Sources */, 34B3F88F1E8DF1710035BE1A /* RegistrationViewController.m in Sources */, 34B3F8901E8DF1710035BE1A /* SettingsTableViewController.m in Sources */, 34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */, @@ -2071,7 +2087,9 @@ 451DE9FD1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */, 45666F761D9BFE00008FE134 /* OWS100RemoveTSRecipientsMigration.m in Sources */, 34B3F89F1E8DF5490035BE1A /* OWSTableViewController.m in Sources */, + 3400C7931EAF89CD008A8584 /* SendExternalFileViewController.m in Sources */, FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */, + 3400C7991EAFB772008A8584 /* ThreadViewHelper.m in Sources */, 76EB054018170B33006006FC /* AppDelegate.m in Sources */, 341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */, 34B3F89C1E8DF3270035BE1A /* BlockListViewController.m in Sources */, diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 73a358d68..c6c886585 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -119,146 +119,11 @@ CFBundleDocumentTypes - CFBundleTypeName - Signal File - CFBundleTypeIconFiles - - Icon - Icon@2x - Icon@3x - LSItemContentTypes - public.item - public.content - public.composite-content - public.message - public.contact public.archive - public.disk-image + public.content public.data - public.directory - com.apple.resolvable - public.symlink - public.executable - com.apple.mount-point - com.apple.alias-file - com.apple.alias-record - com.apple.bookmark - public.url - public.file-url - public.text - public.plain-text - public.utf8-plain-text - public.utf16-external-plain-text - public.utf16-plain-text - public.delimited-values-text - public.comma-separated-values-text - public.tab-separated-values-text - public.utf8-tab-separated-values-text - public.rtf - public.html - public.xml - public.source-code - public.assembly-source - public.c-source - public.objective-c-source - public.swift-source - public.c-plus-plus-source - public.objective-c-plus-plus-source - public.c-header - public.c-plus-plus-header - com.sun.java-source - public.script - com.apple.applescript.text - com.apple.applescript.script - com.apple.applescript.script-bundle - com.netscape.javascript-source - public.shell-script - public.perl-script - public.python-script - public.ruby-script - public.php-script - public.json - com.apple.property-list - com.apple.xml-property-list - com.apple.binary-property-list - com.adobe.pdf - com.apple.rtfd - com.apple.flat-rtfd - com.apple.txn.text-multimedia-data - com.apple.webarchive - public.image - public.jpeg - public.jpeg-2000 - public.tiff - com.apple.pict - com.compuserve.gif - public.png - com.apple.quicktime-image - com.apple.icns - com.microsoft.bmp - com.microsoft.ico - public.camera-raw-image - public.svg-image - com.apple.live-photo - public.audiovisual-content - public.movie - public.video - public.audio - com.apple.quicktime-movie - public.mpeg - public.mpeg-2-video - public.mpeg-2-transport-stream - public.mp3 - public.mpeg-4 - public.mpeg-4-audio - com.apple.protected-mpeg-4-audio - com.apple.protected-mpeg-4-video - public.avi - public.aiff-audio - com.microsoft.waveform-audio - public.midi-audio - public.playlist - public.m3u-playlist - public.folder - public.volume - com.apple.package - com.apple.bundle - com.apple.plugin - com.apple.metadata-importer - com.apple.quicklook-generator - com.apple.xpc-service - com.apple.framework - com.apple.application - com.apple.application-bundle - com.apple.application-file - public.unix-executable - com.microsoft.windows-executable - com.sun.java-class - com.sun.java-archive - com.apple.systempreference.prefpane - org.gnu.gnu-zip-archive - public.bzip2-archive - public.zip-archive - public.spreadsheet - public.presentation - public.database - public.vcard - public.to-do-item - public.calendar-event - public.email-message - com.apple.internet-location - com.apple.ink.inktext - public.font - public.bookmark - public.3d-content - com.rsa.pkcs-12 - public.x509-certificate - org.idpf.epub-container - public.log - com.adobe.pdf - public.pdf LSHandlerRank Alternate diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 7bdb787ca..cc8ccc7e6 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -15,6 +15,7 @@ #import "PropertyListPreferences.h" #import "PushManager.h" #import "Release.h" +#import "SendExternalFileViewController.h" #import "Signal-Swift.h" #import "TSMessagesManager.h" #import "TSSocketManager.h" @@ -294,6 +295,22 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; return NO; } DDLogInfo(@"Application opened with URL: %@", url); + + [[TSAccountManager sharedInstance] + ifRegistered:YES + runAsync:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + SendExternalFileViewController *viewController = [SendExternalFileViewController new]; + viewController.attachment = attachment; + UINavigationController *navigationController = + [[UINavigationController alloc] initWithRootViewController:viewController]; + [[[Environment getCurrent] signalsViewController] + presentTopLevelModalViewController:navigationController + animateDismissal:NO + animatePresentation:YES]; + }); + }]; + return YES; } else { DDLogWarn(@"Application opened with an unknown URL scheme: %@", url.scheme); diff --git a/Signal/src/ViewControllers/InboxTableViewCell.h b/Signal/src/ViewControllers/InboxTableViewCell.h index 3aa7282f1..155632b1e 100644 --- a/Signal/src/ViewControllers/InboxTableViewCell.h +++ b/Signal/src/ViewControllers/InboxTableViewCell.h @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN +@class Contact; @class OWSContactsManager; typedef enum : NSUInteger { kArchiveState = 0, kInboxState = 1 } CellState; @@ -20,13 +21,23 @@ typedef enum : NSUInteger { kArchiveState = 0, kInboxState = 1 } CellState; @property (nonatomic) IBOutlet UIView *contentContainerView; @property (nonatomic) IBOutlet UIView *messageCounter; @property (nonatomic) NSString *threadId; +@property (nonatomic) NSString *contactId; + (instancetype)inboxTableViewCell; ++ (CGFloat)rowHeight; + - (void)configureWithThread:(TSThread *)thread contactsManager:(OWSContactsManager *)contactsManager blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet; +// This method is used to present _possible_ threads - threads +// that will be created if this cell is selected. +- (void)configureWithContact:(Contact *)contact + recipientId:(NSString *)recipientId + contactsManager:(OWSContactsManager *)contactsManager + isBlocked:(BOOL)isBlocked; + - (void)animateDisappear; @end diff --git a/Signal/src/ViewControllers/InboxTableViewCell.m b/Signal/src/ViewControllers/InboxTableViewCell.m index 89a595259..13ec2f167 100644 --- a/Signal/src/ViewControllers/InboxTableViewCell.m +++ b/Signal/src/ViewControllers/InboxTableViewCell.m @@ -5,6 +5,7 @@ #import "InboxTableViewCell.h" #import "Environment.h" #import "OWSAvatarBuilder.h" +#import "OWSContactAvatarBuilder.h" #import "PropertyListPreferences.h" #import "Signal-Swift.h" #import "TSContactThread.h" @@ -25,12 +26,14 @@ NS_ASSUME_NONNULL_BEGIN @interface InboxTableViewCell () -@property NSUInteger unreadMessages; -@property UIView *messagesBadge; -@property UILabel *unreadLabel; +@property (nonatomic) NSUInteger unreadMessages; +@property (nonatomic) UIView *messagesBadge; +@property (nonatomic) UILabel *unreadLabel; @end +#pragma mark - + @implementation InboxTableViewCell + (instancetype)inboxTableViewCell { @@ -41,6 +44,11 @@ NS_ASSUME_NONNULL_BEGIN return cell; } ++ (CGFloat)rowHeight +{ + return 72.f; +} + - (void)initializeLayout { self.selectionStyle = UITableViewCellSelectionStyleDefault; } @@ -130,6 +138,52 @@ NS_ASSUME_NONNULL_BEGIN }); } +- (void)configureWithContact:(Contact *)contact + recipientId:(NSString *)recipientId + contactsManager:(OWSContactsManager *)contactsManager + isBlocked:(BOOL)isBlocked +{ + OWSAssert([NSThread isMainThread]); + OWSAssert(contact); + OWSAssert(recipientId.length > 0); + OWSAssert(contactsManager); + + NSString *name = contact.fullName; + self.threadId = recipientId; + NSMutableAttributedString *snippetText = [NSMutableAttributedString new]; + if (isBlocked) { + // If thread is blocked, don't show a snippet or mute status. + [snippetText + appendAttributedString:[[NSAttributedString alloc] + initWithString:NSLocalizedString(@"HOME_VIEW_BLOCKED_CONTACT_CONVERSATION", + @"A label for conversations with blocked users.") + attributes:@{ + NSFontAttributeName : [UIFont ows_mediumFontWithSize:12], + NSForegroundColorAttributeName : [UIColor ows_blackColor], + }]]; + } + + self.nameLabel.text = name; + self.snippetLabel.attributedText = snippetText; + self.contactPictureView.image = [UIImage imageNamed:@"empty-group-avatar"]; + [UIUtil applyRoundedBorderToImageView:_contactPictureView]; + + self.separatorInset = UIEdgeInsetsMake(0, _contactPictureView.frame.size.width * 1.5f, 0, 0); + + [self updateCellForUnreadMessage]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + UIImage *avatar = [[[OWSContactAvatarBuilder alloc] initWithContactId:recipientId + name:contact.fullName + contactsManager:contactsManager] build]; + dispatch_async(dispatch_get_main_queue(), ^{ + if ([self.threadId isEqualToString:recipientId]) { + self.contactPictureView.image = avatar; + } + }); + }); +} + - (void)updateCellForUnreadMessage { _nameLabel.font = [UIFont ows_boldFontWithSize:14.0f]; _nameLabel.textColor = [UIColor ows_blackColor]; diff --git a/Signal/src/ViewControllers/MessagesViewController.m b/Signal/src/ViewControllers/MessagesViewController.m index 82bca67c5..4a8571102 100644 --- a/Signal/src/ViewControllers/MessagesViewController.m +++ b/Signal/src/ViewControllers/MessagesViewController.m @@ -113,24 +113,10 @@ typedef enum : NSUInteger { return YES; } -- (BOOL)pasteBoardHasText -{ - if ([UIPasteboard generalPasteboard].numberOfItems < 1) { - return NO; - } - NSIndexSet *itemSet = [NSIndexSet indexSetWithIndex:0]; - NSSet *utiTypes = - [NSSet setWithArray:[[UIPasteboard generalPasteboard] pasteboardTypesForItemSet:itemSet][0]]; - return ([utiTypes containsObject:(NSString *)kUTTypeText] || [utiTypes containsObject:(NSString *)kUTTypePlainText] - || - [utiTypes containsObject:(NSString *)kUTTypeUTF8PlainText] || - [utiTypes containsObject:(NSString *)kUTTypeUTF16PlainText]); -} - - (BOOL)pasteBoardHasPossibleAttachment { // We don't want to load/convert images more than once so we // only do a cursory validation pass at this time. - return ([SignalAttachment pasteboardHasPossibleAttachment] && ![self pasteBoardHasText]); + return ([SignalAttachment pasteboardHasPossibleAttachment] && ![SignalAttachment pasteBoardHasText]); } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { diff --git a/Signal/src/ViewControllers/OWSTableViewController.h b/Signal/src/ViewControllers/OWSTableViewController.h index f1129e76e..52d406733 100644 --- a/Signal/src/ViewControllers/OWSTableViewController.h +++ b/Signal/src/ViewControllers/OWSTableViewController.h @@ -35,6 +35,8 @@ extern const CGFloat kOWSTable_DefaultCellHeight; - (void)addItem:(OWSTableItem *)item; +- (NSUInteger)itemCount; + @end #pragma mark - @@ -69,8 +71,18 @@ typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(); #pragma mark - +@protocol OWSTableViewControllerDelegate + +- (void)tableViewDidScroll; + +@end + +#pragma mark - + @interface OWSTableViewController : UIViewController +@property (nonatomic, weak) id delegate; + @property (nonatomic) OWSTableContents *contents; @property (nonatomic, readonly) UITableView *tableView; diff --git a/Signal/src/ViewControllers/OWSTableViewController.m b/Signal/src/ViewControllers/OWSTableViewController.m index db89bbc4c..34907f12d 100644 --- a/Signal/src/ViewControllers/OWSTableViewController.m +++ b/Signal/src/ViewControllers/OWSTableViewController.m @@ -71,6 +71,11 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; [_items addObject:item]; } +- (NSUInteger)itemCount +{ + return _items.count; +} + @end #pragma mark - @@ -171,19 +176,15 @@ NSString * const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier"; @implementation OWSTableViewController -- (void)viewDidLoad -{ - [super viewDidLoad]; - [self.navigationController.navigationBar setTranslucent:NO]; -} - - (void)loadView { [super loadView]; OWSAssert(self.contents); - self.title = self.contents.title; + if (self.contents.title.length > 0) { + self.title = self.contents.title; + } self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped]; self.tableView.delegate = self; @@ -196,6 +197,20 @@ NSString * const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier"; [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kOWSTableCellIdentifier]; } +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.navigationController.navigationBar setTranslucent:NO]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero]; +} + - (OWSTableSection *)sectionForIndex:(NSInteger)sectionIndex { OWSAssert(self.contents); @@ -217,20 +232,27 @@ NSString * const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier"; return item; } +- (void)setContents:(OWSTableContents *)contents +{ + OWSAssert(contents); + + _contents = contents; + + [self.tableView reloadData]; +} + #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { OWSAssert(self.contents); - - OWSAssert(self.contents.sections.count > 0); return (NSInteger) self.contents.sections.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)sectionIndex { OWSTableSection *section = [self sectionForIndex:sectionIndex]; - OWSAssert(section.items.count > 0); + OWSAssert(section.items); return (NSInteger) section.items.count; } @@ -356,6 +378,13 @@ NSString * const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier"; [self dismissViewControllerAnimated:YES completion:nil]; } +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView +{ + [self.delegate tableViewDidScroll]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/SelectThreadViewController.h b/Signal/src/ViewControllers/SelectThreadViewController.h new file mode 100644 index 000000000..dc812826e --- /dev/null +++ b/Signal/src/ViewControllers/SelectThreadViewController.h @@ -0,0 +1,21 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +@class TSThread; + +@protocol SelectThreadViewControllerDelegate + +- (void)threadWasSelected:(TSThread *)thread; + +- (BOOL)canSelectBlockedContact; + +@end + +#pragma mark - + +@interface SelectThreadViewController : UIViewController + +@property (nonatomic, weak) id delegate; + +@end diff --git a/Signal/src/ViewControllers/SelectThreadViewController.m b/Signal/src/ViewControllers/SelectThreadViewController.m new file mode 100644 index 000000000..8ad82ff6a --- /dev/null +++ b/Signal/src/ViewControllers/SelectThreadViewController.m @@ -0,0 +1,428 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "SelectThreadViewController.h" +#import "BlockListUIUtils.h" +#import "ContactTableViewCell.h" +#import "Environment.h" +#import "InboxTableViewCell.h" +#import "OWSContactsManager.h" +#import "OWSContactsSearcher.h" +#import "OWSTableViewController.h" +#import "ThreadViewHelper.h" +#import "UIColor+OWS.h" +#import "UIFont+OWS.h" +#import "UIView+OWS.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SelectThreadViewController () + +@property (nonatomic, readonly) OWSBlockingManager *blockingManager; +@property (nonatomic) NSSet *blockedPhoneNumberSet; + +@property (nonatomic, readonly) OWSContactsManager *contactsManager; +@property (nonatomic) NSArray *contacts; + +@property (nonatomic, readonly) ThreadViewHelper *threadViewHelper; + +@property (nonatomic, readonly) OWSTableViewController *tableViewController; +@property (nonatomic, readonly) UISearchBar *searchBar; + +@end + +#pragma mark - + +@implementation SelectThreadViewController + +- (void)loadView +{ + [super loadView]; + + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop + target:self + action:@selector(dismissPressed:)]; + + self.view.backgroundColor = [UIColor whiteColor]; + + _blockingManager = [OWSBlockingManager sharedManager]; + _blockedPhoneNumberSet = [NSSet setWithArray:[_blockingManager blockedPhoneNumbers]]; + _contactsManager = [Environment getCurrent].contactsManager; + self.contacts = [self filteredContacts]; + _threadViewHelper = [ThreadViewHelper new]; + _threadViewHelper.delegate = self; + + [self createViews]; + + [self addNotificationListeners]; + + [self updateTableContents]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [self.navigationController.navigationBar setTranslucent:NO]; +} + +- (void)addNotificationListeners +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(blockedPhoneNumbersDidChange:) + name:kNSNotificationName_BlockedPhoneNumbersDidChange + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(signalRecipientsDidChange:) + name:OWSContactsManagerSignalRecipientsDidChangeNotification + object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)createViews +{ + // Table + _tableViewController = [OWSTableViewController new]; + _tableViewController.delegate = self; + _tableViewController.contents = [OWSTableContents new]; + [self.view addSubview:self.tableViewController.view]; + [_tableViewController.view autoPinWidthToSuperview]; + [_tableViewController.view autoPinToTopLayoutGuideOfViewController:self withInset:0]; + [_tableViewController.view autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + + // Search + UISearchBar *searchBar = [UISearchBar new]; + _searchBar = searchBar; + searchBar.searchBarStyle = UISearchBarStyleProminent; + searchBar.delegate = self; + searchBar.placeholder = NSLocalizedString(@"SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT", @""); + [searchBar sizeToFit]; + _tableViewController.tableView.tableHeaderView = searchBar; +} + +#pragma mark - UISearchBarDelegate + +- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText +{ + [self updateTableContents]; +} + +- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar +{ + [self updateTableContents]; +} + +- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar +{ + [self updateTableContents]; +} + +- (void)searchBarResultsListButtonClicked:(UISearchBar *)searchBar +{ + [self updateTableContents]; +} + +- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope +{ + [self updateTableContents]; +} + +#pragma mark - Actions + +- (void)updateTableContents +{ + __weak SelectThreadViewController *weakSelf = self; + OWSTableContents *contents = [OWSTableContents new]; + OWSTableSection *section = [OWSTableSection new]; + + // Threads + for (TSThread *thread in [self filteredThreadsWithSearchText]) { + [section addItem:[OWSTableItem itemWithCustomCellBlock:^{ + SelectThreadViewController *strongSelf = weakSelf; + if (!strongSelf) { + return (InboxTableViewCell *)nil; + } + + InboxTableViewCell *cell = [InboxTableViewCell inboxTableViewCell]; + + [cell configureWithThread:thread + contactsManager:strongSelf.contactsManager + blockedPhoneNumberSet:strongSelf.blockedPhoneNumberSet]; + + return cell; + } + customRowHeight:[InboxTableViewCell rowHeight] + actionBlock:^{ + [weakSelf.delegate threadWasSelected:thread]; + }]]; + } + + // Contacts + NSArray *filteredContacts = [self filteredContactsWithSearchText]; + for (Contact *contact in filteredContacts) { + [section addItem:[OWSTableItem itemWithCustomCellBlock:^{ + SelectThreadViewController *strongSelf = weakSelf; + if (!strongSelf) { + return (InboxTableViewCell *)nil; + } + + // To be consistent with the threads (above), we use InboxTableViewCell + // instead of ContactTableViewCell to present contacts. + InboxTableViewCell *cell = [InboxTableViewCell inboxTableViewCell]; + + // TODO: Use ContactAccount. + NSString *recipientId = contact.textSecureIdentifiers.firstObject; + BOOL isBlocked = [strongSelf isContactBlocked:contact]; + + [cell configureWithContact:contact + recipientId:recipientId + contactsManager:strongSelf.contactsManager + isBlocked:isBlocked]; + + return cell; + } + customRowHeight:[InboxTableViewCell rowHeight] + actionBlock:^{ + [weakSelf contactWasSelected:contact]; + }]]; + } + + if (section.itemCount < 1) { + [section addItem:[OWSTableItem itemWithCustomCellBlock:^{ + UITableViewCell *cell = [UITableViewCell new]; + cell.textLabel.text = NSLocalizedString( + @"SETTINGS_BLOCK_LIST_NO_CONTACTS", @"A label that indicates the user has no Signal contacts."); + cell.textLabel.font = [UIFont ows_regularFontWithSize:15.f]; + cell.textLabel.textColor = [UIColor colorWithWhite:0.5f alpha:1.f]; + cell.textLabel.textAlignment = NSTextAlignmentCenter; + return cell; + } + actionBlock:nil]]; + } + [contents addSection:section]; + + self.tableViewController.contents = contents; +} + +- (void)contactWasSelected:(Contact *)contact +{ + OWSAssert(contact); + OWSAssert(self.delegate); + + // TODO: Use ContactAccount. + NSString *recipientId = contact.textSecureIdentifiers.firstObject; + + if ([self isRecipientIdBlocked:recipientId] && + ![self.delegate canSelectBlockedContact]) { + + __weak SelectThreadViewController *weakSelf = self; + [BlockListUIUtils showUnblockContactActionSheet:contact + fromViewController:self + blockingManager:self.blockingManager + contactsManager:self.contactsManager + completionBlock:^(BOOL isBlocked) { + if (!isBlocked) { + [weakSelf contactWasSelected:contact]; + } + }]; + return; + } + + __block TSThread *thread = nil; + [[TSStorageManager sharedManager].dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + thread = [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction]; + }]; + OWSAssert(thread); + + [self.delegate threadWasSelected:thread]; +} + +#pragma mark - Filter + +- (NSArray *)filteredThreadsWithSearchText +{ + NSArray *threads = self.threadViewHelper.threads; + + NSString *searchTerm = + [[self.searchBar text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if ([searchTerm isEqualToString:@""]) { + return threads; + } + + NSString *formattedNumber = [PhoneNumber removeFormattingCharacters:searchTerm]; + + NSMutableArray *result = [NSMutableArray new]; + for (TSThread *thread in threads) { + if ([thread.name containsString:searchTerm]) { + [result addObject:thread]; + } else if ([thread isKindOfClass:[TSContactThread class]]) { + TSContactThread *contactThread = (TSContactThread *)thread; + if (formattedNumber.length > 0 && [contactThread.contactIdentifier containsString:formattedNumber]) { + [result addObject:thread]; + } + } + } + return result; +} + +// TODO: Move this to contacts view helper. +- (NSArray *)filteredContactsWithSearchText +{ + // We don't want to show a 1:1 thread with Alice and Alice's contact, + // so we de-duplicate by recipientId. + NSArray *threads = self.threadViewHelper.threads; + NSMutableSet *contactIdsToIgnore = [NSMutableSet new]; + for (TSThread *thread in threads) { + if ([thread isKindOfClass:[TSContactThread class]]) { + TSContactThread *contactThread = (TSContactThread *)thread; + [contactIdsToIgnore addObject:contactThread.contactIdentifier]; + } + } + + NSString *searchString = [self.searchBar text]; + + NSArray *nonRedundantContacts = + [self.contacts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(Contact *contact, + NSDictionary *_Nullable bindings) { + return ![contactIdsToIgnore containsObject:contact.textSecureIdentifiers.firstObject]; + }]]; + + // TODO: Move this to contacts view helper. + OWSContactsSearcher *contactsSearcher = [[OWSContactsSearcher alloc] initWithContacts:nonRedundantContacts]; + NSArray *filteredContacts = [contactsSearcher filterWithString:searchString]; + + return filteredContacts; +} + +#pragma mark - Contacts and Blocking + +- (void)blockedPhoneNumbersDidChange:(id)notification +{ + dispatch_async(dispatch_get_main_queue(), ^{ + _blockedPhoneNumberSet = [NSSet setWithArray:[_blockingManager blockedPhoneNumbers]]; + + [self updateContacts]; + }); +} + +- (void)signalRecipientsDidChange:(NSNotification *)notification +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateContacts]; + }); +} + +- (void)updateContacts +{ + OWSAssert([NSThread isMainThread]); + + self.contacts = [self filteredContacts]; + [self updateTableContents]; +} + +- (BOOL)isContactBlocked:(Contact *)contact +{ + if (contact.parsedPhoneNumbers.count < 1) { + // Hide contacts without any valid phone numbers. + return NO; + } + + for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) { + if ([_blockedPhoneNumberSet containsObject:phoneNumber.toE164]) { + return YES; + } + } + + return NO; +} + +- (BOOL)isRecipientIdBlocked:(NSString *)recipientId +{ + OWSAssert(recipientId.length > 0); + + return [_blockedPhoneNumberSet containsObject:recipientId]; +} + +- (BOOL)isContactHidden:(Contact *)contact +{ + if (contact.parsedPhoneNumbers.count < 1) { + // Hide contacts without any valid phone numbers. + return YES; + } + + if ([self isCurrentUserContact:contact]) { + return YES; + } + + return NO; +} + +- (BOOL)isCurrentUserContact:(Contact *)contact +{ + for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) { + if ([[phoneNumber toE164] isEqualToString:[TSAccountManager localNumber]]) { + return YES; + } + } + + return NO; +} + +- (NSArray *_Nonnull)filteredContacts +{ + NSMutableArray *result = [NSMutableArray new]; + for (Contact *contact in self.contactsManager.signalContacts) { + if (![self isContactHidden:contact]) { + [result addObject:contact]; + } + } + return [result copy]; +} + +#pragma mark - Events + +- (void)dismissPressed:(id)sender +{ + [self.searchBar resignFirstResponder]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - OWSTableViewControllerDelegate + +- (void)tableViewDidScroll +{ + [self.searchBar resignFirstResponder]; +} + +#pragma mark - ThreadViewHelperDelegate + +- (void)threadListDidChange +{ + [self updateTableContents]; +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/SendExternalFileViewController.h b/Signal/src/ViewControllers/SendExternalFileViewController.h new file mode 100644 index 000000000..b0c04f44d --- /dev/null +++ b/Signal/src/ViewControllers/SendExternalFileViewController.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "SelectThreadViewController.h" + +@class SignalAttachment; + +@interface SendExternalFileViewController : SelectThreadViewController + +@property (nonatomic) SignalAttachment *attachment; + +@end diff --git a/Signal/src/ViewControllers/SendExternalFileViewController.m b/Signal/src/ViewControllers/SendExternalFileViewController.m new file mode 100644 index 000000000..19ad05022 --- /dev/null +++ b/Signal/src/ViewControllers/SendExternalFileViewController.m @@ -0,0 +1,66 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "SendExternalFileViewController.h" +#import "Environment.h" +#import "Signal-Swift.h" +#import "ThreadUtil.h" +#import +#import + +@interface SendExternalFileViewController () + +@property (nonatomic, readonly) OWSMessageSender *messageSender; + +@end + +@implementation SendExternalFileViewController + +- (void)loadView +{ + [super loadView]; + + self.delegate = self; + + _messageSender = [Environment getCurrent].messageSender; + + self.title = NSLocalizedString(@"SEND_EXTERNAL_FILE_VIEW_TITLE", @"Title for the 'send external file' view."); +} + +#pragma mark - SelectThreadViewControllerDelegate + +- (void)threadWasSelected:(TSThread *)thread +{ + OWSAssert(self.attachment); + OWSAssert(thread); + + // We should have a valid filename. + OWSAssert(self.attachment.filename.length > 0); + NSString *fileExtension = [self.attachment.filename pathExtension].lowercaseString; + OWSAssert(fileExtension.length > 0); + NSSet *textExtensions = [NSSet setWithArray:@[ + @"txt", + @"url", + ]]; + NSString *text = nil; + if ([textExtensions containsObject:fileExtension]) { + text = [[NSString alloc] initWithData:self.attachment.data encoding:NSUTF8StringEncoding]; + OWSAssert(text); + } + + if (text) { + [ThreadUtil sendMessageWithText:text inThread:thread messageSender:self.messageSender]; + } else { + [ThreadUtil sendMessageWithAttachment:self.attachment inThread:thread messageSender:self.messageSender]; + } + + [Environment messageThreadId:thread.uniqueId]; +} + +- (BOOL)canSelectBlockedContact +{ + return NO; +} + +@end diff --git a/Signal/src/ViewControllers/SignalAttachment.swift b/Signal/src/ViewControllers/SignalAttachment.swift index 4727b92c3..c5462596e 100644 --- a/Signal/src/ViewControllers/SignalAttachment.swift +++ b/Signal/src/ViewControllers/SignalAttachment.swift @@ -204,17 +204,16 @@ class SignalAttachment: NSObject { // Returns the file extension for this attachment or nil if no file extension // can be identified. var fileExtension: String? { - if dataUTI == SignalAttachment.kOversizeTextAttachmentUTI || - dataUTI == SignalAttachment.kUnknownTestAttachmentUTI { - assertionFailure() + if dataUTI == SignalAttachment.kOversizeTextAttachmentUTI { + return "txt" + } + if dataUTI == SignalAttachment.kUnknownTestAttachmentUTI { + return "unknown" + } + guard let fileExtension = MIMETypeUtil.fileExtension(forUTIType:dataUTI) else { return nil } - - guard let fileExtension = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, - kUTTagClassFilenameExtension) else { - return nil - } - return fileExtension.takeRetainedValue() as String + return fileExtension } // Returns the set of UTIs that correspond to valid _input_ image formats @@ -250,6 +249,16 @@ class SignalAttachment: NSObject { return MIMETypeUtil.supportedAudioUTITypes() } + public class var textUTISet: Set { + return [ + kUTTypeText as String, + kUTTypePlainText as String, + kUTTypeUTF8PlainText as String, + kUTTypeUTF16PlainText as String, + kUTTypeURL as String, + ] + } + public var isImage: Bool { return SignalAttachment.outputImageUTISet.contains(dataUTI) } @@ -266,10 +275,26 @@ class SignalAttachment: NSObject { return SignalAttachment.audioUTISet.contains(dataUTI) } + public var isText: Bool { + return SignalAttachment.textUTISet.contains(dataUTI) + } + public class func pasteboardHasPossibleAttachment() -> Bool { return UIPasteboard.general.numberOfItems > 0 } + public class func pasteBoardHasText() -> Bool { + if UIPasteboard.general.numberOfItems < 1 { + return false + } + let itemSet = IndexSet(integer:0) + guard let pasteboardUTITypes = UIPasteboard.general.types(forItemSet:itemSet) else { + return false + } + let pasteboardUTISet = Set(pasteboardUTITypes[0]) + return pasteboardUTISet.intersection(textUTISet).count > 0 + } + // Returns an attachment from the pasteboard, or nil if no attachment // can be found. // diff --git a/Signal/src/ViewControllers/SignalsViewController.h b/Signal/src/ViewControllers/SignalsViewController.h index b24c0dfd7..314fe30ee 100644 --- a/Signal/src/ViewControllers/SignalsViewController.h +++ b/Signal/src/ViewControllers/SignalsViewController.h @@ -20,4 +20,11 @@ - (NSNumber *)updateInboxCountLabel; - (void)composeNew; +- (void)presentTopLevelModalViewController:(UIViewController *)viewController + animateDismissal:(BOOL)animateDismissal + animatePresentation:(BOOL)animatePresentation; +- (void)pushTopLevelViewController:(UIViewController *)viewController + animateDismissal:(BOOL)animateDismissal + animatePresentation:(BOOL)animatePresentation; + @end diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index d4bb955bd..6b07a2539 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -528,7 +528,6 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - - (void)presentThread:(TSThread *)thread keyboardOnViewAppearing:(BOOL)keyboardOnViewAppearing callOnViewAppearing:(BOOL)callOnViewAppearing @@ -549,6 +548,67 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS }); } +- (void)presentTopLevelModalViewController:(UIViewController *)viewController + animateDismissal:(BOOL)animateDismissal + animatePresentation:(BOOL)animatePresentation +{ + OWSAssert([NSThread isMainThread]); + OWSAssert(viewController); + + [self presentViewControllerWithBlock:^{ + [self presentViewController:viewController animated:animatePresentation completion:nil]; + } + animateDismissal:animateDismissal]; +} + +- (void)pushTopLevelViewController:(UIViewController *)viewController + animateDismissal:(BOOL)animateDismissal + animatePresentation:(BOOL)animatePresentation +{ + OWSAssert([NSThread isMainThread]); + OWSAssert(viewController); + + [self presentViewControllerWithBlock:^{ + [self.navigationController pushViewController:viewController animated:animatePresentation]; + } + animateDismissal:animateDismissal]; +} + +- (void)presentViewControllerWithBlock:(void (^)())presentationBlock animateDismissal:(BOOL)animateDismissal +{ + OWSAssert([NSThread isMainThread]); + OWSAssert(presentationBlock); + + // Presenting a "top level" view controller has three steps: + // + // First, dismiss any presented modal. + // Second, pop to the root view controller if necessary. + // Third present the new view controller using presentationBlock. + + // Define a block to perform the second step. + void (^dismissNavigationBlock)() = ^{ + if (self.navigationController.viewControllers.lastObject != self) { + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + presentationBlock(); + }]; + + [self.navigationController popToViewController:self animated:animateDismissal]; + + [CATransaction commit]; + } else { + presentationBlock(); + } + }; + + // Perform the first step. + if (self.presentedViewController) { + [self.presentedViewController dismissViewControllerAnimated:animateDismissal completion:dismissNavigationBlock]; + } else { + dismissNavigationBlock(); + } +} + #pragma mark - Navigation - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { diff --git a/Signal/src/ViewControllers/ThreadViewHelper.h b/Signal/src/ViewControllers/ThreadViewHelper.h new file mode 100644 index 000000000..86367e4ec --- /dev/null +++ b/Signal/src/ViewControllers/ThreadViewHelper.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +@protocol ThreadViewHelperDelegate + +- (void)threadListDidChange; + +@end + +#pragma mark - + +@class TSThread; + +// A helper class +@interface ThreadViewHelper : NSObject + +@property (nonatomic, weak) id delegate; + +@property (nonatomic, readonly) NSMutableArray *threads; + +@end diff --git a/Signal/src/ViewControllers/ThreadViewHelper.m b/Signal/src/ViewControllers/ThreadViewHelper.m new file mode 100644 index 000000000..757aabd73 --- /dev/null +++ b/Signal/src/ViewControllers/ThreadViewHelper.m @@ -0,0 +1,136 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "ThreadViewHelper.h" +#import +#import +#import +#import +#import +#import + +@interface ThreadViewHelper () + +@property (nonatomic) YapDatabaseConnection *uiDatabaseConnection; +@property (nonatomic) YapDatabaseViewMappings *threadMappings; + +@end + +@implementation ThreadViewHelper + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(yapDatabaseModified:) + name:TSUIDatabaseConnectionDidUpdateNotification + object:nil]; + [self initializeMapping]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)initializeMapping +{ + OWSAssert([NSThread isMainThread]); + + NSString *grouping = TSInboxGroup; + + self.threadMappings = + [[YapDatabaseViewMappings alloc] initWithGroups:@[ grouping ] view:TSThreadDatabaseViewExtensionName]; + [self.threadMappings setIsReversed:YES forGroup:grouping]; + + __weak ThreadViewHelper *weakSelf = self; + [self.uiDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) { + [self.threadMappings updateWithTransaction:transaction]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateThreads]; + [weakSelf.delegate threadListDidChange]; + }); + }]; +} + +#pragma mark - Database + +- (YapDatabaseConnection *)uiDatabaseConnection +{ + NSAssert([NSThread isMainThread], @"Must access uiDatabaseConnection on main thread!"); + if (!_uiDatabaseConnection) { + YapDatabase *database = TSStorageManager.sharedManager.database; + _uiDatabaseConnection = [database newConnection]; + [_uiDatabaseConnection beginLongLivedReadTransaction]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(yapDatabaseModified:) + name:YapDatabaseModifiedNotification + object:database]; + } + return _uiDatabaseConnection; +} + +- (void)yapDatabaseModified:(NSNotification *)notification +{ + OWSAssert([NSThread isMainThread]); + + NSArray *notifications = [self.uiDatabaseConnection beginLongLivedReadTransaction]; + NSArray *sectionChanges = nil; + NSArray *rowChanges = nil; + [[self.uiDatabaseConnection ext:TSThreadDatabaseViewExtensionName] getSectionChanges:§ionChanges + rowChanges:&rowChanges + forNotifications:notifications + withMappings:self.threadMappings]; + if (sectionChanges.count == 0 && rowChanges.count == 0) { + // Ignore irrelevant modifications. + return; + } + + [self updateThreads]; + + [self.delegate threadListDidChange]; +} + +- (void)updateThreads +{ + OWSAssert([NSThread isMainThread]); + + NSMutableArray *threads = [NSMutableArray new]; + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + NSUInteger numberOfSections = [self.threadMappings numberOfSections]; + OWSAssert(numberOfSections == 1); + for (NSUInteger section = 0; section < numberOfSections; section++) { + NSUInteger numberOfItems = [self.threadMappings numberOfItemsInSection:section]; + for (NSUInteger item = 0; item < numberOfItems; item++) { + TSThread *thread = [[transaction extension:TSThreadDatabaseViewExtensionName] + objectAtIndexPath:[NSIndexPath indexPathForItem:(NSInteger)item inSection:(NSInteger)section] + withMappings:self.threadMappings]; + [threads addObject:thread]; + } + } + }]; + + _threads = threads; +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +@end diff --git a/Signal/src/contact/OWSContactsSearcher.m b/Signal/src/contact/OWSContactsSearcher.m index 9c44693fe..f776e444d 100644 --- a/Signal/src/contact/OWSContactsSearcher.m +++ b/Signal/src/contact/OWSContactsSearcher.m @@ -1,9 +1,5 @@ // -// OWSContactsSearcher.m -// Signal -// -// Created by Michael Kirk on 6/27/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "OWSContactsSearcher.h" @@ -34,6 +30,7 @@ NSString *formattedNumber = [PhoneNumber removeFormattingCharacters:searchTerm]; + // TODO: This assumes there's a single search term. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(fullName contains[c] %@) OR (ANY parsedPhoneNumbers.toE164 contains[c] %@)", searchTerm, formattedNumber]; return [self.contacts filteredArrayUsingPredicate:predicate]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 0cdff1500..247e7711e 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -699,7 +699,7 @@ /* No comment provided by engineer. */ "NEW_GROUP_DEFAULT_TITLE" = "New Group"; -/* No comment provided by engineer. */ +/* Placeholder text for group name field */ "NEW_GROUP_NAMEGROUP_REQUEST_DEFAULT" = "Name this group chat"; /* No comment provided by engineer. */ @@ -910,12 +910,21 @@ /* No comment provided by engineer. */ "SECURE_SESSION_RESET" = "Secure session was reset."; +/* A label for the 'select contact' mode vs. 'select thread' mode. */ +"SELECT_CONTACT_MODE_LABEL" = "Contacts"; + +/* A label for the 'select thread' mode vs. 'select contact' mode. */ +"SELECT_THREAD_MODE_LABEL" = "Conversations"; + /* No comment provided by engineer. */ "SEND_AGAIN_BUTTON" = "Send Again"; /* No comment provided by engineer. */ "SEND_BUTTON_TITLE" = "Send"; +/* Title for the 'send external file' view. */ +"SEND_EXTERNAL_FILE_VIEW_TITLE" = "Send File"; + /* Alert body after invite failed */ "SEND_INVITE_FAILURE" = "Sending invite failed, please try again later.";