Merge branch 'charlesmchen/bundleDocumentTypes'

pull/1/head
Matthew Chen 8 years ago
commit 8c1c38b305

@ -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 = "<group>"; };
3400C7911EAF89CD008A8584 /* SendExternalFileViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SendExternalFileViewController.m; sourceTree = "<group>"; };
3400C7941EAF99F4008A8584 /* SelectThreadViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SelectThreadViewController.h; sourceTree = "<group>"; };
3400C7951EAF99F4008A8584 /* SelectThreadViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SelectThreadViewController.m; sourceTree = "<group>"; };
3400C7971EAFB772008A8584 /* ThreadViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadViewHelper.h; sourceTree = "<group>"; };
3400C7981EAFB772008A8584 /* ThreadViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadViewHelper.m; sourceTree = "<group>"; };
341BB7471DB727EE001E2975 /* JSQMediaItem+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMediaItem+OWS.h"; sourceTree = "<group>"; };
341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMediaItem+OWS.m"; sourceTree = "<group>"; };
34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "fontawesome-webfont.ttf"; sourceTree = "<group>"; };
@ -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 */,

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "file-thin-black-w-shadow-large.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

@ -116,5 +116,18 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>LSItemContentTypes</key>
<array>
<string>public.archive</string>
<string>public.content</string>
<string>public.data</string>
</array>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
</dict>
</plist>

@ -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"
@ -263,6 +264,54 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
} else {
DDLogWarn(@"Application opened with an unknown URL action: %@", url.host);
}
} else if ([url.scheme.lowercaseString isEqualToString:@"file"]) {
NSString *filename = url.lastPathComponent;
if ([filename stringByDeletingPathExtension].length < 1) {
DDLogError(@"Application opened with URL invalid filename: %@", url);
return NO;
}
NSString *fileExtension = [filename pathExtension];
if (fileExtension.length < 1) {
DDLogError(@"Application opened with URL missing file extension: %@", url);
return NO;
}
NSString *utiType = [MIMETypeUtil utiTypeForFileExtension:fileExtension];
if (utiType.length < 1) {
DDLogError(@"Application opened with URL of unknown UTI type: %@", url);
return NO;
}
NSData *data = [NSData dataWithContentsOfURL:url];
if (!data) {
DDLogError(@"Application opened with URL with unloadable content: %@", url);
return NO;
}
SignalAttachment *attachment = [SignalAttachment attachmentWithData:data dataUTI:utiType filename:filename];
if (!attachment) {
DDLogError(@"Application opened with URL with invalid content: %@", url);
return NO;
}
if ([attachment hasError]) {
DDLogError(@"Application opened with URL with content error: %@ %@", url, [attachment errorName]);
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);
}

@ -131,8 +131,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)performEditingAction:(SEL)action
{
if (action == @selector(copy:)) {
UIPasteboard *pasteBoard = UIPasteboard.generalPasteboard;
[pasteBoard setData:self.fileData forPasteboardType:(__bridge NSString *)kUTTypeGIF];
UIPasteboard *pasteboard = UIPasteboard.generalPasteboard;
[pasteboard setData:self.fileData forPasteboardType:(__bridge NSString *)kUTTypeGIF];
} else if (action == NSSelectorFromString(@"save:")) {
NSData *photoData = self.fileData;
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];

@ -26,9 +26,9 @@ class AttachmentApprovalViewController: UIViewController, OWSAudioAttachmentPlay
@available(*, unavailable, message:"use attachment: constructor instead.")
required init?(coder aDecoder: NSCoder) {
self.attachment = SignalAttachment.genericAttachment(data: nil,
dataUTI: kUTTypeContent as String,
filename:nil)
self.attachment = SignalAttachment.attachment(data: nil,
dataUTI: kUTTypeContent as String,
filename:nil)
super.init(coder: aDecoder)
assertionFailure()
}
@ -217,7 +217,7 @@ class AttachmentApprovalViewController: UIViewController, OWSAudioAttachmentPlay
private func createGenericPreview(attachmentPreviewView: UIView) {
var subviews = [UIView]()
let imageView = createHeroImageView(imageName: "file-thin-black-large")
let imageView = createHeroImageView(imageName: "file-thin-black-filled-large")
subviews.append(imageView)
if let fileNameLabel = createFileNameLabel() {
@ -245,6 +245,11 @@ class AttachmentApprovalViewController: UIViewController, OWSAudioAttachmentPlay
let imageView = UIImageView(image:image)
imageView.layer.minificationFilter = kCAFilterTrilinear
imageView.layer.magnificationFilter = kCAFilterTrilinear
imageView.layer.shadowColor = UIColor.black.cgColor
let shadowScaling = 5.0
imageView.layer.shadowRadius = CGFloat(2.0 * shadowScaling)
imageView.layer.shadowOpacity = 0.25
imageView.layer.shadowOffset = CGSize(width: 0.75 * shadowScaling, height: 0.75 * shadowScaling)
imageView.autoSetDimension(.width, toSize:imageSize)
imageView.autoSetDimension(.height, toSize:imageSize)

@ -139,8 +139,10 @@ NS_ASSUME_NONNULL_BEGIN
for (int i=0; i < 32; i++) {
[message appendString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rutrum, nulla vitae pretium hendrerit, tellus turpis pharetra libero, vitae sodales tortor ante vel sem. Fusce sed nisl a lorem gravida tincidunt. Suspendisse efficitur non quam ac sodales. Aenean ut velit maximus, posuere sem a, accumsan nunc. Donec ullamcorper turpis lorem. Quisque dignissim purus eu placerat ultricies. Proin at urna eget mi semper congue. Aenean non elementum ex. Praesent pharetra quam at sem vestibulum, vestibulum ornare dolor elementum. Vestibulum massa tortor, scelerisque sit amet pulvinar a, rhoncus vitae nisl. Sed mi nunc, tempus at varius in, malesuada vitae dui. Vivamus efficitur pulvinar erat vitae congue. Proin vehicula turpis non felis congue facilisis. Nullam aliquet dapibus ligula ac mollis. Etiam sit amet posuere lorem, in rhoncus nisi."];
}
SignalAttachment *attachment = [SignalAttachment oversizeTextAttachmentWithText:message];
SignalAttachment *attachment = [SignalAttachment attachmentWithData:[message dataUsingEncoding:NSUTF8StringEncoding]
dataUTI:SignalAttachment.kOversizeTextAttachmentUTI
filename:nil];
[ThreadUtil sendMessageWithAttachment:attachment
inThread:thread
messageSender:messageSender];
@ -163,7 +165,7 @@ NS_ASSUME_NONNULL_BEGIN
uti:(NSString *)uti {
OWSMessageSender *messageSender = [Environment getCurrent].messageSender;
SignalAttachment *attachment =
[SignalAttachment genericAttachmentWithData:[self createRandomNSDataOfSize:256] dataUTI:uti filename:nil];
[SignalAttachment attachmentWithData:[self createRandomNSDataOfSize:256] dataUTI:uti filename:nil];
[ThreadUtil sendMessageWithAttachment:attachment
inThread:thread
messageSender:messageSender];

@ -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<NSString *> *)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

@ -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];

@ -113,29 +113,16 @@ typedef enum : NSUInteger {
return YES;
}
- (BOOL)pasteBoardHasText
- (BOOL)pasteboardHasPossibleAttachment
{
if ([UIPasteboard generalPasteboard].numberOfItems < 1) {
return NO;
}
NSIndexSet *itemSet = [NSIndexSet indexSetWithIndex:0];
NSSet<NSString *> *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 {
if (action == @selector(paste:)) {
if ([self pasteBoardHasPossibleAttachment]) {
if ([self pasteboardHasPossibleAttachment]) {
return YES;
}
}
@ -143,7 +130,7 @@ typedef enum : NSUInteger {
}
- (void)paste:(id)sender {
if ([self pasteBoardHasPossibleAttachment]) {
if ([self pasteboardHasPossibleAttachment]) {
SignalAttachment *attachment = [SignalAttachment attachmentFromPasteboard];
// Note: attachment might be nil or have an error at this point; that's fine.
[self.textViewPasteDelegate didPasteAttachment:attachment];
@ -1168,7 +1155,10 @@ typedef enum : NSUInteger {
// which are presented as normal text messages.
const NSUInteger kOversizeTextMessageSizeThreshold = 16 * 1024;
if ([text lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
SignalAttachment *attachment = [SignalAttachment oversizeTextAttachmentWithText:text];
SignalAttachment *attachment =
[SignalAttachment attachmentWithData:[text dataUsingEncoding:NSUTF8StringEncoding]
dataUTI:SignalAttachment.kOversizeTextAttachmentUTI
filename:nil];
[ThreadUtil sendMessageWithAttachment:attachment inThread:self.thread messageSender:self.messageSender];
} else {
[ThreadUtil sendMessageWithText:text inThread:self.thread messageSender:self.messageSender];
@ -2423,7 +2413,7 @@ typedef enum : NSUInteger {
OWSAssert([NSThread isMainThread]);
SignalAttachment *attachment =
[SignalAttachment imageAttachmentWithData:imageData dataUTI:dataUTI filename:filename];
[SignalAttachment attachmentWithData:imageData dataUTI:dataUTI filename:filename];
[self dismissViewControllerAnimated:YES
completion:^{
OWSAssert([NSThread isMainThread]);
@ -2488,7 +2478,7 @@ typedef enum : NSUInteger {
NSData *videoData = [NSData dataWithContentsOfURL:compressedVideoUrl];
dispatch_async(dispatch_get_main_queue(), ^{
SignalAttachment *attachment =
[SignalAttachment videoAttachmentWithData:videoData dataUTI:(NSString *)kUTTypeMPEG4 filename:filename];
[SignalAttachment attachmentWithData:videoData dataUTI:(NSString *)kUTTypeMPEG4 filename:filename];
if (!attachment || [attachment hasError]) {
DDLogWarn(@"%@ %s Invalid attachment: %@.",
self.tag,
@ -2715,7 +2705,7 @@ typedef enum : NSUInteger {
if (flag) {
NSData *audioData = [NSData dataWithContentsOfURL:recorder.url];
SignalAttachment *attachment =
[SignalAttachment audioAttachmentWithData:audioData dataUTI:(NSString *)kUTTypeMPEG4Audio filename:nil];
[SignalAttachment attachmentWithData:audioData dataUTI:(NSString *)kUTTypeMPEG4Audio filename:nil];
if (!attachment ||
[attachment hasError]) {
DDLogWarn(@"%@ %s Invalid attachment: %@.",

@ -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 <NSObject>
- (void)tableViewDidScroll;
@end
#pragma mark -
@interface OWSTableViewController : UIViewController
@property (nonatomic, weak) id<OWSTableViewControllerDelegate> delegate;
@property (nonatomic) OWSTableContents *contents;
@property (nonatomic, readonly) UITableView *tableView;

@ -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

@ -0,0 +1,27 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
@class TSThread;
NS_ASSUME_NONNULL_BEGIN
@protocol SelectThreadViewControllerDelegate <NSObject>
- (void)threadWasSelected:(TSThread *)thread;
- (BOOL)canSelectBlockedContact;
- (nullable UIView *)createHeaderWithSearchBar:(UISearchBar *)searchBar;
@end
#pragma mark -
@interface SelectThreadViewController : UIViewController
@property (nonatomic, weak) id<SelectThreadViewControllerDelegate> delegate;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,416 @@
//
// 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 <SignalServiceKit/OWSBlockingManager.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSContactThread.h>
#import <SignalServiceKit/TSThread.h>
NS_ASSUME_NONNULL_BEGIN
@interface SelectThreadViewController () <OWSTableViewControllerDelegate, ThreadViewHelperDelegate, UISearchBarDelegate>
@property (nonatomic, readonly) OWSBlockingManager *blockingManager;
@property (nonatomic) NSSet<NSString *> *blockedPhoneNumberSet;
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
@property (nonatomic) NSArray<Contact *> *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
{
OWSAssert(self.delegate);
// Search
UISearchBar *searchBar = [UISearchBar new];
_searchBar = searchBar;
searchBar.searchBarStyle = UISearchBarStyleMinimal;
searchBar.delegate = self;
searchBar.placeholder = NSLocalizedString(@"SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT", @"");
searchBar.backgroundColor = [UIColor whiteColor];
[searchBar sizeToFit];
UIView *header = [self.delegate createHeaderWithSearchBar:searchBar];
// 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];
if (header) {
_tableViewController.tableView.tableHeaderView = header;
} else {
_tableViewController.tableView.tableHeaderView = searchBar;
}
[_tableViewController.view autoPinEdgeToSuperviewEdge:ALEdgeBottom];
}
#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 (ContactTableViewCell *)nil;
}
// To be consistent with the threads (above), we use ContactTableViewCell
// instead of InboxTableViewCell to present contacts and threads.
ContactTableViewCell *cell = [ContactTableViewCell new];
[cell configureWithThread:thread contactsManager:strongSelf.contactsManager];
return cell;
}
customRowHeight:[ContactTableViewCell rowHeight]
actionBlock:^{
[weakSelf.delegate threadWasSelected:thread];
}]];
}
// Contacts
NSArray<Contact *> *filteredContacts = [self filteredContactsWithSearchText];
for (Contact *contact in filteredContacts) {
[section addItem:[OWSTableItem itemWithCustomCellBlock:^{
SelectThreadViewController *strongSelf = weakSelf;
if (!strongSelf) {
return (ContactTableViewCell *)nil;
}
ContactTableViewCell *cell = [ContactTableViewCell new];
BOOL isBlocked = [strongSelf isContactBlocked:contact];
if (isBlocked) {
cell.accessoryMessage
= NSLocalizedString(@"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked.");
} else {
OWSAssert(cell.accessoryMessage == nil);
}
[cell configureWithContact:contact contactsManager:strongSelf.contactsManager];
return cell;
}
customRowHeight:[ContactTableViewCell 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<TSThread *> *)filteredThreadsWithSearchText
{
NSArray<TSThread *> *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<Contact *> *)filteredContactsWithSearchText
{
// We don't want to show a 1:1 thread with Alice and Alice's contact,
// so we de-duplicate by recipientId.
NSArray<TSThread *> *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<NSString *, id> *_Nullable bindings) {
return ![contactIdsToIgnore containsObject:contact.textSecureIdentifiers.firstObject];
}]];
// TODO: Move this to contacts view helper.
OWSContactsSearcher *contactsSearcher = [[OWSContactsSearcher alloc] initWithContacts:nonRedundantContacts];
NSArray<Contact *> *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;
}
return NO;
}
- (NSArray<Contact *> *_Nonnull)filteredContacts
{
NSMutableArray<Contact *> *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

@ -0,0 +1,17 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "SelectThreadViewController.h"
NS_ASSUME_NONNULL_BEGIN
@class SignalAttachment;
@interface SendExternalFileViewController : SelectThreadViewController
@property (nonatomic) SignalAttachment *attachment;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,158 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "SendExternalFileViewController.h"
#import "Environment.h"
#import "Signal-Swift.h"
#import "ThreadUtil.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/TSThread.h>
NS_ASSUME_NONNULL_BEGIN
@interface SendExternalFileViewController () <SelectThreadViewControllerDelegate>
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@end
#pragma mark -
@implementation SendExternalFileViewController
- (instancetype)init
{
if (self = [super init]) {
self.delegate = self;
}
return self;
}
- (void)loadView
{
[super loadView];
_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);
[ThreadUtil sendMessageWithAttachment:self.attachment inThread:thread messageSender:self.messageSender];
[Environment messageThreadId:thread.uniqueId];
}
- (BOOL)canSelectBlockedContact
{
return NO;
}
- (nullable UIView *)createHeaderWithSearchBar:(UISearchBar *)searchBar
{
OWSAssert(searchBar)
const CGFloat imageSize
= ScaleFromIPhone5To7Plus(40, 50);
const CGFloat imageLabelSpacing = ScaleFromIPhone5To7Plus(5, 8);
const CGFloat titleVSpacing = ScaleFromIPhone5To7Plus(10, 15);
const CGFloat contentVMargin = 20;
UIView *header = [UIView new];
header.backgroundColor = [UIColor whiteColor];
UIView *titleLabel = [self createTitleLabel];
[titleLabel sizeToFit];
[header addSubview:titleLabel];
[titleLabel autoHCenterInSuperview];
[titleLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:contentVMargin];
UIView *fileView = [UIView new];
[header addSubview:fileView];
[fileView autoHCenterInSuperview];
[fileView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:titleLabel withOffset:titleVSpacing];
UIImage *image = [UIImage imageNamed:@"file-thin-black-filled-large"];
OWSAssert(image);
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.layer.minificationFilter = kCAFilterTrilinear;
imageView.layer.magnificationFilter = kCAFilterTrilinear;
imageView.layer.shadowColor = [UIColor blackColor].CGColor;
imageView.layer.shadowRadius = 2.f;
imageView.layer.shadowOpacity = 0.2f;
imageView.layer.shadowOffset = CGSizeMake(0.75f, 0.75f);
[fileView addSubview:imageView];
[imageView autoSetDimension:ALDimensionWidth toSize:imageSize];
[imageView autoSetDimension:ALDimensionHeight toSize:imageSize];
[imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft];
[imageView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[imageView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
UIView *fileNameLabel = [self createFileNameLabel];
[fileView addSubview:fileNameLabel];
[fileNameLabel autoAlignAxis:ALAxisHorizontal toSameAxisOfView:imageView];
[fileNameLabel autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:imageView withOffset:imageLabelSpacing];
[fileNameLabel autoPinEdgeToSuperviewEdge:ALEdgeRight];
[header addSubview:searchBar];
[searchBar autoPinWidthToSuperview];
[searchBar autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:fileView withOffset:contentVMargin];
[searchBar autoPinEdgeToSuperviewEdge:ALEdgeBottom];
// UITableViewController.tableHeaderView must have its height set.
header.frame = CGRectMake(0,
0,
0,
(contentVMargin * 2 + titleLabel.frame.size.height + titleVSpacing + imageSize + searchBar.frame.size.height));
return header;
}
- (NSString *)formattedFileName
{
OWSAssert(self.attachment) NSString *filename =
[self.attachment.filename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
OWSAssert(filename.length > 0);
const NSUInteger kMaxFilenameLength = 50;
if (filename.length > kMaxFilenameLength) {
// Truncate the filename if necessary.
//
// TODO: Use l10n-safe truncation.
filename = [[[filename substringToIndex:kMaxFilenameLength / 2] stringByAppendingString:@"…"]
stringByAppendingString:[filename substringFromIndex:filename.length - kMaxFilenameLength / 2]];
}
return filename;
}
- (UIView *)createFileNameLabel
{
UILabel *label = [UILabel new];
label.text = [self formattedFileName];
label.textColor = [UIColor ows_materialBlueColor];
label.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(16.f, 20.f)];
return label;
}
- (UIView *)createTitleLabel
{
UILabel *label = [UILabel new];
label.text
= NSLocalizedString(@"SEND_EXTERNAL_FILE_HEADER_TITLE", @"Header title for the 'send external file' view.");
label.textColor = [UIColor blackColor];
label.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(18.f, 20.f)];
return label;
}
@end
NS_ASSUME_NONNULL_END

@ -12,6 +12,7 @@ enum SignalAttachmentError: Error {
case couldNotParseImage
case couldNotConvertToJpeg
case invalidFileFormat
case unknownType
}
extension SignalAttachmentError: LocalizedError {
@ -29,6 +30,8 @@ extension SignalAttachmentError: LocalizedError {
return NSLocalizedString("ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG", comment: "Attachment error message for image attachments which could not be converted to JPEG")
case .invalidFileFormat:
return NSLocalizedString("ATTACHMENT_ERROR_INVALID_FILE_FORMAT", comment: "Attachment error message for attachments with an invalid file format")
case .unknownType:
return NSLocalizedString("ATTACHMENT_ERROR_UNKNOWN_TYPE", comment: "Attachment error message for attachments with an invalid file format")
}
}
}
@ -111,6 +114,10 @@ class SignalAttachment: NSObject {
self.dataUTI = dataUTI
self.filename = filename
super.init()
if self.mimeType == nil {
error = .unknownType
}
}
// MARK: Methods
@ -204,17 +211,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()
return nil
if dataUTI == SignalAttachment.kOversizeTextAttachmentUTI {
return "txt"
}
guard let fileExtension = UTTypeCopyPreferredTagWithClass(dataUTI as CFString,
kUTTagClassFilenameExtension) else {
if dataUTI == SignalAttachment.kUnknownTestAttachmentUTI {
return "unknown"
}
guard let fileExtension = MIMETypeUtil.fileExtension(forUTIType:dataUTI) else {
return nil
}
return fileExtension.takeRetainedValue() as String
return fileExtension
}
// Returns the set of UTIs that correspond to valid _input_ image formats
@ -250,6 +256,16 @@ class SignalAttachment: NSObject {
return MIMETypeUtil.supportedAudioUTITypes()
}
public class var textUTISet: Set<String> {
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 +282,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<String>(pasteboardUTITypes[0])
return pasteboardUTISet.intersection(textUTISet).count > 0
}
// Returns an attachment from the pasteboard, or nil if no attachment
// can be found.
//
@ -353,7 +385,7 @@ class SignalAttachment: NSObject {
//
// NOTE: The attachment returned by this method may not be valid.
// Check the attachment's error property.
public class func imageAttachment(data imageData: Data?, dataUTI: String, filename: String?) -> SignalAttachment {
private class func imageAttachment(data imageData: Data?, dataUTI: String, filename: String?) -> SignalAttachment {
assert(dataUTI.characters.count > 0)
assert(imageData != nil)
@ -539,7 +571,7 @@ class SignalAttachment: NSObject {
//
// NOTE: The attachment returned by this method may not be valid.
// Check the attachment's error property.
public class func videoAttachment(data: Data?, dataUTI: String, filename: String?) -> SignalAttachment {
private class func videoAttachment(data: Data?, dataUTI: String, filename: String?) -> SignalAttachment {
return newAttachment(data : data,
dataUTI : dataUTI,
validUTISet : videoUTISet,
@ -553,7 +585,7 @@ class SignalAttachment: NSObject {
//
// NOTE: The attachment returned by this method may not be valid.
// Check the attachment's error property.
public class func audioAttachment(data: Data?, dataUTI: String, filename: String?) -> SignalAttachment {
private class func audioAttachment(data: Data?, dataUTI: String, filename: String?) -> SignalAttachment {
return newAttachment(data : data,
dataUTI : dataUTI,
validUTISet : audioUTISet,
@ -567,12 +599,12 @@ class SignalAttachment: NSObject {
//
// NOTE: The attachment returned by this method may not be valid.
// Check the attachment's error property.
public class func oversizeTextAttachment(text: String?) -> SignalAttachment {
private class func oversizeTextAttachment(text: String?) -> SignalAttachment {
return newAttachment(data : text?.data(using: .utf8),
dataUTI : kOversizeTextAttachmentUTI,
validUTISet : nil,
maxFileSize : kMaxFileSizeGeneric,
filename : nil)
filename : nil)
}
// MARK: Generic Attachments
@ -581,7 +613,7 @@ class SignalAttachment: NSObject {
//
// NOTE: The attachment returned by this method may not be valid.
// Check the attachment's error property.
public class func genericAttachment(data: Data?, dataUTI: String, filename: String?) -> SignalAttachment {
private class func genericAttachment(data: Data?, dataUTI: String, filename: String?) -> SignalAttachment {
return newAttachment(data : data,
dataUTI : dataUTI,
validUTISet : nil,

@ -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

@ -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 {

@ -0,0 +1,30 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@protocol ThreadViewHelperDelegate <NSObject>
- (void)threadListDidChange;
@end
#pragma mark -
@class TSThread;
// A helper class for views that want to present the list of threads
// that show up in home view, and in the same order.
//
// It observes changes to the threads & their ordering and informs
// its delegate when they happen.
@interface ThreadViewHelper : NSObject
@property (nonatomic, weak) id<ThreadViewHelperDelegate> delegate;
@property (nonatomic, readonly) NSMutableArray<TSThread *> *threads;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,140 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "ThreadViewHelper.h"
#import <SignalServiceKit/TSDatabaseView.h>
#import <SignalServiceKit/TSStorageManager.h>
#import <SignalServiceKit/TSThread.h>
#import <YapDatabase/YapDatabaseConnection.h>
#import <YapDatabase/YapDatabaseViewChange.h>
#import <YapDatabase/YapDatabaseViewConnection.h>
NS_ASSUME_NONNULL_BEGIN
@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:&sectionChanges
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<TSThread *> *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 copy];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

@ -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];

@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
extern NSString *const kContactsTable_CellReuseIdentifier;
@class OWSContactsManager;
@class TSThread;
@interface ContactTableViewCell : UITableViewCell
@ -29,6 +30,8 @@ extern NSString *const kContactsTable_CellReuseIdentifier;
- (void)configureWithRecipientId:(NSString *)recipientId contactsManager:(OWSContactsManager *)contactsManager;
- (void)configureWithThread:(TSThread *)thread contactsManager:(OWSContactsManager *)contactsManager;
@end
NS_ASSUME_NONNULL_END

@ -9,6 +9,8 @@
#import "UIFont+OWS.h"
#import "UIUtil.h"
#import "UIView+OWS.h"
#import <SignalServiceKit/TSGroupThread.h>
#import <SignalServiceKit/TSThread.h>
NS_ASSUME_NONNULL_BEGIN
@ -50,8 +52,8 @@ NSString *const kContactsTable_CellReuseIdentifier = @"kContactsTable_CellReuseI
{
const CGFloat kAvatarSize = 40.f;
_avatarView = [UIImageView new];
_avatarView.contentMode = UIViewContentModeScaleToFill;
_avatarView.image = [UIImage imageNamed:@"empty-group-avatar"];
_avatarView.contentMode = UIViewContentModeScaleToFill;
// applyRoundedBorderToImageView requires the avatar to have
// the correct size.
_avatarView.frame = CGRectMake(0, 0, kAvatarSize, kAvatarSize);
@ -121,6 +123,28 @@ NSString *const kContactsTable_CellReuseIdentifier = @"kContactsTable_CellReuseI
[self layoutSubviews];
}
- (void)configureWithThread:(TSThread *)thread contactsManager:(OWSContactsManager *)contactsManager
{
OWSAssert(thread);
NSString *threadName = thread.name;
if (threadName.length == 0 && [thread isKindOfClass:[TSGroupThread class]]) {
threadName = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"");
}
NSAttributedString *attributedText = [[NSAttributedString alloc]
initWithString:threadName
attributes:@{
NSForegroundColorAttributeName : [UIColor blackColor],
}];
self.nameLabel.attributedText = attributedText;
self.avatarView.image = [OWSAvatarBuilder buildImageForThread:thread contactsManager:contactsManager];
// Force layout, since imageView isn't being initally rendered on App Store optimized build.
[self layoutSubviews];
}
- (void)layoutSubviews
{
[super layoutSubviews];

@ -94,6 +94,9 @@
/* Attachment error message for attachments without any data */
"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty.";
/* Attachment error message for attachments with an invalid file format */
"ATTACHMENT_ERROR_UNKNOWN_TYPE" = "Attachment is of invalid type";
/* Accessibility hint describing what you can do with the attachment button */
"ATTACHMENT_HINT" = "Choose or take a picture and then send it";
@ -699,7 +702,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. */
@ -916,6 +919,12 @@
/* No comment provided by engineer. */
"SEND_BUTTON_TITLE" = "Send";
/* Header title for the 'send external file' view. */
"SEND_EXTERNAL_FILE_HEADER_TITLE" = "Select a Recipient for:";
/* 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.";

Loading…
Cancel
Save