mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Let users share imported files to a thread or contact of their choice.
// FREEBIE
This commit is contained in:
parent
3c7574a908
commit
6e36ce97a5
19 changed files with 956 additions and 180 deletions
|
@ -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 */,
|
||||
|
|
|
@ -119,146 +119,11 @@
|
|||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Signal File</string>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array>
|
||||
<string>Icon</string>
|
||||
<string>Icon@2x</string>
|
||||
<string>Icon@3x</string>
|
||||
</array>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.item</string>
|
||||
<string>public.content</string>
|
||||
<string>public.composite-content</string>
|
||||
<string>public.message</string>
|
||||
<string>public.contact</string>
|
||||
<string>public.archive</string>
|
||||
<string>public.disk-image</string>
|
||||
<string>public.content</string>
|
||||
<string>public.data</string>
|
||||
<string>public.directory</string>
|
||||
<string>com.apple.resolvable</string>
|
||||
<string>public.symlink</string>
|
||||
<string>public.executable</string>
|
||||
<string>com.apple.mount-point</string>
|
||||
<string>com.apple.alias-file</string>
|
||||
<string>com.apple.alias-record</string>
|
||||
<string>com.apple.bookmark</string>
|
||||
<string>public.url</string>
|
||||
<string>public.file-url</string>
|
||||
<string>public.text</string>
|
||||
<string>public.plain-text</string>
|
||||
<string>public.utf8-plain-text</string>
|
||||
<string>public.utf16-external-plain-text</string>
|
||||
<string>public.utf16-plain-text</string>
|
||||
<string>public.delimited-values-text</string>
|
||||
<string>public.comma-separated-values-text</string>
|
||||
<string>public.tab-separated-values-text</string>
|
||||
<string>public.utf8-tab-separated-values-text</string>
|
||||
<string>public.rtf</string>
|
||||
<string>public.html</string>
|
||||
<string>public.xml</string>
|
||||
<string>public.source-code</string>
|
||||
<string>public.assembly-source</string>
|
||||
<string>public.c-source</string>
|
||||
<string>public.objective-c-source</string>
|
||||
<string>public.swift-source</string>
|
||||
<string>public.c-plus-plus-source</string>
|
||||
<string>public.objective-c-plus-plus-source</string>
|
||||
<string>public.c-header</string>
|
||||
<string>public.c-plus-plus-header</string>
|
||||
<string>com.sun.java-source</string>
|
||||
<string>public.script</string>
|
||||
<string>com.apple.applescript.text</string>
|
||||
<string>com.apple.applescript.script</string>
|
||||
<string>com.apple.applescript.script-bundle</string>
|
||||
<string>com.netscape.javascript-source</string>
|
||||
<string>public.shell-script</string>
|
||||
<string>public.perl-script</string>
|
||||
<string>public.python-script</string>
|
||||
<string>public.ruby-script</string>
|
||||
<string>public.php-script</string>
|
||||
<string>public.json</string>
|
||||
<string>com.apple.property-list</string>
|
||||
<string>com.apple.xml-property-list</string>
|
||||
<string>com.apple.binary-property-list</string>
|
||||
<string>com.adobe.pdf</string>
|
||||
<string>com.apple.rtfd</string>
|
||||
<string>com.apple.flat-rtfd</string>
|
||||
<string>com.apple.txn.text-multimedia-data</string>
|
||||
<string>com.apple.webarchive</string>
|
||||
<string>public.image</string>
|
||||
<string>public.jpeg</string>
|
||||
<string>public.jpeg-2000</string>
|
||||
<string>public.tiff</string>
|
||||
<string>com.apple.pict</string>
|
||||
<string>com.compuserve.gif</string>
|
||||
<string>public.png</string>
|
||||
<string>com.apple.quicktime-image</string>
|
||||
<string>com.apple.icns</string>
|
||||
<string>com.microsoft.bmp</string>
|
||||
<string>com.microsoft.ico</string>
|
||||
<string>public.camera-raw-image</string>
|
||||
<string>public.svg-image</string>
|
||||
<string>com.apple.live-photo</string>
|
||||
<string>public.audiovisual-content</string>
|
||||
<string>public.movie</string>
|
||||
<string>public.video</string>
|
||||
<string>public.audio</string>
|
||||
<string>com.apple.quicktime-movie</string>
|
||||
<string>public.mpeg</string>
|
||||
<string>public.mpeg-2-video</string>
|
||||
<string>public.mpeg-2-transport-stream</string>
|
||||
<string>public.mp3</string>
|
||||
<string>public.mpeg-4</string>
|
||||
<string>public.mpeg-4-audio</string>
|
||||
<string>com.apple.protected-mpeg-4-audio</string>
|
||||
<string>com.apple.protected-mpeg-4-video</string>
|
||||
<string>public.avi</string>
|
||||
<string>public.aiff-audio</string>
|
||||
<string>com.microsoft.waveform-audio</string>
|
||||
<string>public.midi-audio</string>
|
||||
<string>public.playlist</string>
|
||||
<string>public.m3u-playlist</string>
|
||||
<string>public.folder</string>
|
||||
<string>public.volume</string>
|
||||
<string>com.apple.package</string>
|
||||
<string>com.apple.bundle</string>
|
||||
<string>com.apple.plugin</string>
|
||||
<string>com.apple.metadata-importer</string>
|
||||
<string>com.apple.quicklook-generator</string>
|
||||
<string>com.apple.xpc-service</string>
|
||||
<string>com.apple.framework</string>
|
||||
<string>com.apple.application</string>
|
||||
<string>com.apple.application-bundle</string>
|
||||
<string>com.apple.application-file</string>
|
||||
<string>public.unix-executable</string>
|
||||
<string>com.microsoft.windows-executable</string>
|
||||
<string>com.sun.java-class</string>
|
||||
<string>com.sun.java-archive</string>
|
||||
<string>com.apple.systempreference.prefpane</string>
|
||||
<string>org.gnu.gnu-zip-archive</string>
|
||||
<string>public.bzip2-archive</string>
|
||||
<string>public.zip-archive</string>
|
||||
<string>public.spreadsheet</string>
|
||||
<string>public.presentation</string>
|
||||
<string>public.database</string>
|
||||
<string>public.vcard</string>
|
||||
<string>public.to-do-item</string>
|
||||
<string>public.calendar-event</string>
|
||||
<string>public.email-message</string>
|
||||
<string>com.apple.internet-location</string>
|
||||
<string>com.apple.ink.inktext</string>
|
||||
<string>public.font</string>
|
||||
<string>public.bookmark</string>
|
||||
<string>public.3d-content</string>
|
||||
<string>com.rsa.pkcs-12</string>
|
||||
<string>public.x509-certificate</string>
|
||||
<string>org.idpf.epub-container</string>
|
||||
<string>public.log</string>
|
||||
<string>com.adobe.pdf</string>
|
||||
<string>public.pdf</string>
|
||||
</array>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,24 +113,10 @@ typedef enum : NSUInteger {
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)pasteBoardHasText
|
||||
{
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
|
|
21
Signal/src/ViewControllers/SelectThreadViewController.h
Normal file
21
Signal/src/ViewControllers/SelectThreadViewController.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
@class TSThread;
|
||||
|
||||
@protocol SelectThreadViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)threadWasSelected:(TSThread *)thread;
|
||||
|
||||
- (BOOL)canSelectBlockedContact;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface SelectThreadViewController : UIViewController
|
||||
|
||||
@property (nonatomic, weak) id<SelectThreadViewControllerDelegate> delegate;
|
||||
|
||||
@end
|
428
Signal/src/ViewControllers/SelectThreadViewController.m
Normal file
428
Signal/src/ViewControllers/SelectThreadViewController.m
Normal file
|
@ -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 <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
|
||||
{
|
||||
// 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<Contact *> *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<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;
|
||||
}
|
||||
|
||||
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<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
|
13
Signal/src/ViewControllers/SendExternalFileViewController.h
Normal file
13
Signal/src/ViewControllers/SendExternalFileViewController.h
Normal file
|
@ -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
|
66
Signal/src/ViewControllers/SendExternalFileViewController.m
Normal file
66
Signal/src/ViewControllers/SendExternalFileViewController.m
Normal file
|
@ -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 <SignalServiceKit/OWSMessageSender.h>
|
||||
#import <SignalServiceKit/TSThread.h>
|
||||
|
||||
@interface SendExternalFileViewController () <SelectThreadViewControllerDelegate>
|
||||
|
||||
@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<NSString *> *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
|
|
@ -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<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 +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<String>(pasteboardUTITypes[0])
|
||||
return pasteboardUTISet.intersection(textUTISet).count > 0
|
||||
}
|
||||
|
||||
// Returns an attachment from the pasteboard, or nil if no attachment
|
||||
// can be found.
|
||||
//
|
||||
|
|
|
@ -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 {
|
||||
|
|
22
Signal/src/ViewControllers/ThreadViewHelper.h
Normal file
22
Signal/src/ViewControllers/ThreadViewHelper.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
@protocol ThreadViewHelperDelegate <NSObject>
|
||||
|
||||
- (void)threadListDidChange;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@class TSThread;
|
||||
|
||||
// A helper class
|
||||
@interface ThreadViewHelper : NSObject
|
||||
|
||||
@property (nonatomic, weak) id<ThreadViewHelperDelegate> delegate;
|
||||
|
||||
@property (nonatomic, readonly) NSMutableArray<TSThread *> *threads;
|
||||
|
||||
@end
|
136
Signal/src/ViewControllers/ThreadViewHelper.m
Normal file
136
Signal/src/ViewControllers/ThreadViewHelper.m
Normal file
|
@ -0,0 +1,136 @@
|
|||
//
|
||||
// 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>
|
||||
|
||||
@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<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;
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@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];
|
||||
|
|
|
@ -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.";
|
||||
|
||||
|
|
Loading…
Reference in a new issue