session-ios/Signal/src/view controllers/MessagesViewController.m

1794 lines
78 KiB
Mathematica
Raw Normal View History

2014-10-29 21:58:58 +01:00
//
// MessagesViewController.m
// Signal
//
// Created by Dylan Bourgeois on 28/10/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "AppDelegate.h"
#import <AddressBookUI/AddressBookUI.h>
#import <ContactsUI/CNContactViewController.h>
2014-10-29 21:58:58 +01:00
#import "MessagesViewController.h"
#import "FullImageViewController.h"
2014-12-04 00:23:36 +01:00
#import "FingerprintViewController.h"
#import "NewGroupViewController.h"
#import "ShowGroupMembersViewController.h"
#import "SignalKeyingStorage.h"
2014-10-29 21:58:58 +01:00
#import "JSQCallCollectionViewCell.h"
2014-11-24 21:51:43 +01:00
#import "UIUtil.h"
#import "DJWActionSheet+OWS.h"
2014-10-29 21:58:58 +01:00
#import <MobileCoreServices/UTCoreTypes.h>
2014-11-25 16:38:33 +01:00
#import "TSDatabaseView.h"
#import "UIButton+OWS.h"
2014-11-25 16:38:33 +01:00
#import <YapDatabase/YapDatabaseView.h>
2014-12-11 00:05:41 +01:00
2014-11-25 16:38:33 +01:00
#import "TSMessageAdapter.h"
2014-12-11 00:05:41 +01:00
#import "TSErrorMessage.h"
#import "TSInvalidIdentityKeyErrorMessage.h"
#import "TSIncomingMessage.h"
2015-02-19 01:04:32 +01:00
#import "TSAttachmentPointer.h"
#import "TSVideoAttachmentAdapter.h"
2014-11-25 16:38:33 +01:00
#import "TSMessagesManager+sendMessages.h"
2014-12-22 00:40:15 +01:00
#import "TSMessagesManager+attachments.h"
2014-11-25 16:38:33 +01:00
#import "NSDate+millisecondTimeStamp.h"
#import "Environment.h"
#import "PhoneManager.h"
#import "ContactsManager.h"
2014-12-26 21:17:43 +01:00
#import "PreferencesUtil.h"
#import "TSAdapterCacheManager.h"
#define kYapDatabaseRangeLength 50
#define kYapDatabaseRangeMaxLength 300
#define kYapDatabaseRangeMinLength 20
#define JSQ_TOOLBAR_ICON_HEIGHT 22
#define JSQ_TOOLBAR_ICON_WIDTH 22
#define JSQ_IMAGE_INSET 5
2014-11-25 16:38:33 +01:00
static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * 60;
static NSString *const kUpdateGroupSegueIdentifier = @"updateGroupSegue";
static NSString *const kFingerprintSegueIdentifier = @"fingerprintSegue";
static NSString *const kShowGroupMembersSegue = @"showGroupMembersSegue";
2014-10-29 21:58:58 +01:00
typedef enum : NSUInteger {
kMediaTypePicture,
kMediaTypeVideo,
} kMediaTypes;
@interface MessagesViewController () {
UIImage* tappedImage;
BOOL isGroupConversation;
UIView *_unreadContainer;
UIImageView *_unreadBackground;
UILabel *_unreadLabel;
NSUInteger _unreadCount;
2014-10-29 21:58:58 +01:00
}
@property (nonatomic, weak) UIView *navView;
2014-11-26 16:00:10 +01:00
@property (nonatomic, retain) TSThread *thread;
@property (nonatomic, strong) YapDatabaseConnection *editingDatabaseConnection;
@property (nonatomic, strong) YapDatabaseConnection *uiDatabaseConnection;
2014-11-25 16:38:33 +01:00
@property (nonatomic, strong) YapDatabaseViewMappings *messageMappings;
@property (nonatomic, retain) JSQMessagesBubbleImage *outgoingBubbleImageData;
@property (nonatomic, retain) JSQMessagesBubbleImage *incomingBubbleImageData;
@property (nonatomic, retain) JSQMessagesBubbleImage *currentlyOutgoingBubbleImageData;
@property (nonatomic, retain) JSQMessagesBubbleImage *outgoingMessageFailedImageData;
@property (nonatomic, strong) NSTimer *audioPlayerPoller;
2015-01-25 00:48:40 +01:00
@property (nonatomic, strong) TSVideoAttachmentAdapter *currentMediaAdapter;
@property (nonatomic, retain) NSTimer *readTimer;
@property (nonatomic, retain) UIButton *messageButton;
@property (nonatomic, retain) UIButton *attachButton;
2014-11-25 16:38:33 +01:00
@property (nonatomic, retain) NSIndexPath *lastDeliveredMessageIndexPath;
@property (nonatomic, retain) UIGestureRecognizer *showFingerprintDisplay;
@property (nonatomic, retain) UITapGestureRecognizer *toggleContactPhoneDisplay;
@property (nonatomic) BOOL displayPhoneAsTitle;
@property NSUInteger page;
2015-01-31 12:00:58 +01:00
@property BOOL isVisible;
@property (nonatomic) BOOL composeOnOpen;
@property (nonatomic) BOOL peek;
2015-01-31 12:00:58 +01:00
2014-10-29 21:58:58 +01:00
@end
@interface UINavigationItem(){
UIView *backButtonView;
}
@end
2014-10-29 21:58:58 +01:00
@implementation MessagesViewController
2015-03-19 01:59:44 +01:00
- (void)setupWithTSIdentifier:(NSString *)identifier {
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
self.thread = [TSContactThread getOrCreateThreadWithContactId:identifier transaction:transaction];
2014-11-26 16:00:10 +01:00
}];
}
- (void)setupWithTSGroup:(TSGroupModel*)model {
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
self.thread = [TSGroupThread getOrCreateThreadWithGroupModel:model transaction:transaction];
}];
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:self.thread messageBody:@"" attachments:[[NSMutableArray alloc] init]];
message.groupMetaMessage = TSGroupMessageNew;
if(model.groupImage != nil) {
[[TSMessagesManager sharedManager] sendAttachment:UIImagePNGRepresentation(model.groupImage) contentType:@"image/png" inMessage:message thread:self.thread];
}
else {
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
[[TSMessagesManager sharedManager] sendMessage:message inThread:self.thread success:nil failure:nil];
}
isGroupConversation = YES;
}
- (void)peekSetup {
_peek = YES;
[self setComposeOnOpen:NO];
}
- (void)popped {
_peek = NO;
[self hideInputIfNeeded];
}
- (void)setComposeOnOpen:(BOOL)compose {
_composeOnOpen = compose;
}
2015-03-19 01:59:44 +01:00
- (void)setupWithThread:(TSThread *)thread {
2014-11-26 16:00:10 +01:00
self.thread = thread;
isGroupConversation = [self.thread isKindOfClass:[TSGroupThread class]];
2014-11-26 16:00:10 +01:00
}
- (TSThread *)thread {
return _thread;
}
- (void)hideInputIfNeeded {
if (_peek) {
[self inputToolbar].hidden = YES;
return;
}
if([_thread isKindOfClass:[TSGroupThread class]] && ![((TSGroupThread*)_thread).groupModel.groupMemberIds containsObject:[SignalKeyingStorage.localNumber toE164]]) {
[self inputToolbar].hidden= YES; // user has requested they leave the group. further sends disallowed
self.navigationItem.rightBarButtonItem = nil; // further group action disallowed
} else if(![self isTextSecureReachable] ){
[self inputToolbar].hidden= YES; // only RedPhone
} else {
[self inputToolbar].hidden= NO;
[self loadDraftInCompose];
}
}
2015-01-31 12:00:58 +01:00
2014-10-29 21:58:58 +01:00
- (void)viewDidLoad {
[super viewDidLoad];
2015-01-31 12:00:58 +01:00
_isVisible = NO;
[self.navigationController.navigationBar setTranslucent:NO];
_showFingerprintDisplay = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(showFingerprint)];
_toggleContactPhoneDisplay = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleContactPhone)];
_toggleContactPhoneDisplay.numberOfTapsRequired = 1;
_messageButton = [UIButton ows_blueButtonWithTitle:NSLocalizedString(@"SEND_BUTTON_TITLE", @"")];
_messageButton.enabled = FALSE;
2015-03-20 14:41:00 +01:00
_messageButton.titleLabel.adjustsFontSizeToFitWidth = YES;
_attachButton = [[UIButton alloc] init];
[_attachButton setFrame:CGRectMake(0, 0, JSQ_TOOLBAR_ICON_WIDTH+JSQ_IMAGE_INSET*2, JSQ_TOOLBAR_ICON_HEIGHT+JSQ_IMAGE_INSET*2)];
_attachButton.imageEdgeInsets = UIEdgeInsetsMake(JSQ_IMAGE_INSET, JSQ_IMAGE_INSET, JSQ_IMAGE_INSET, JSQ_IMAGE_INSET);
[_attachButton setImage:[UIImage imageNamed:@"btnAttachments--blue"] forState:UIControlStateNormal];
[self markAllMessagesAsRead];
[self initializeBubbles];
[self initializeTextView];
self.messageMappings = [[YapDatabaseViewMappings alloc] initWithGroups:@[self.thread.uniqueId]
view:TSMessageDatabaseViewExtensionName];
self.page = 0;
[self updateRangeOptionsForPage:self.page];
2015-01-31 12:00:58 +01:00
[self.uiDatabaseConnection beginLongLivedReadTransaction];
[self.uiDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) {
2014-11-25 16:38:33 +01:00
[self.messageMappings updateWithTransaction:transaction];
[self.collectionView reloadData];
2014-11-25 16:38:33 +01:00
}];
[self initializeCollectionViewLayout];
self.senderId = ME_MESSAGE_IDENTIFIER
self.senderDisplayName = ME_MESSAGE_IDENTIFIER
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startReadTimer)
name:UIApplicationWillEnterForegroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cancelReadTimer)
name:UIApplicationDidEnterBackgroundNotification object:nil];
2015-02-06 10:45:32 +01:00
self.navigationController.interactivePopGestureRecognizer.delegate = self; // Swipe back to inbox fix. See http://stackoverflow.com/questions/19054625/changing-back-button-in-ios-7-disables-swipe-to-navigate-back
}
- (void)initializeTextView {
[self.inputToolbar.contentView.textView setFont:[UIFont ows_regularFontWithSize:17.f]];
self.inputToolbar.contentView.leftBarButtonItem = _attachButton;
self.inputToolbar.contentView.rightBarButtonItem = _messageButton;
}
2015-03-19 01:59:44 +01:00
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self initializeToolbars];
2015-01-31 12:00:58 +01:00
[self.collectionView reloadData];
NSInteger numberOfMessages = (NSInteger)[self.messageMappings numberOfItemsInGroup:self.thread.uniqueId];
if (numberOfMessages > 0) {
NSIndexPath * lastCellIndexPath = [NSIndexPath indexPathForRow:numberOfMessages-1 inSection:0];
[self.collectionView scrollToItemAtIndexPath:lastCellIndexPath atScrollPosition:UICollectionViewScrollPositionBottom animated:NO];
}
}
2015-03-19 01:59:44 +01:00
- (void)startReadTimer {
self.readTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(markAllMessagesAsRead) userInfo:nil repeats:YES];
}
2015-03-19 01:59:44 +01:00
- (void)cancelReadTimer {
[self.readTimer invalidate];
}
2015-03-19 01:59:44 +01:00
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self markAllMessagesAsRead];
[self startReadTimer];
2015-01-31 12:00:58 +01:00
_isVisible = YES;
[self initializeTitleLabelGestureRecognizer];
[self updateBackButtonAsync];
if (_composeOnOpen) {
[self popKeyBoard];
}
}
- (void)updateBackButtonAsync {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSUInteger count = [[TSMessagesManager sharedManager] unreadMessagesCountExcept:self.thread];
dispatch_async(dispatch_get_main_queue() , ^{
if (self) {
[self setUnreadCount:count];
}
});
});
}
2015-03-19 01:59:44 +01:00
- (void)viewWillDisappear:(BOOL)animated {
if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
// back button was pressed.
[self.navController hideDropDown:self];
}
[super viewWillDisappear:animated];
[_unreadContainer removeFromSuperview];
_unreadContainer = nil;
[_audioPlayerPoller invalidate];
[_audioPlayer stop];
// reset all audio bars to 0
JSQMessagesCollectionView *collectionView = self.collectionView;
NSInteger num_bubbles = [self collectionView:collectionView numberOfItemsInSection:0];
for (NSInteger i=0; i<num_bubbles; i++) {
NSIndexPath *index_path = [NSIndexPath indexPathForRow:i inSection:0];
TSMessageAdapter *msgAdapter = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:index_path];
if (msgAdapter.messageType == TSIncomingMessageAdapter && msgAdapter.isMediaMessage && [msgAdapter isKindOfClass:[TSVideoAttachmentAdapter class]]) {
TSVideoAttachmentAdapter* msgMedia = (TSVideoAttachmentAdapter*)[msgAdapter media];
if ([msgMedia isAudio]) {
msgMedia.isPaused = NO;
msgMedia.isAudioPlaying = NO;
[msgMedia setAudioProgressFromFloat:0];
[msgMedia setAudioIconToPlay];
}
}
}
[self cancelReadTimer];
[self removeTitleLabelGestureRecognizer];
[self saveDraft];
2014-10-29 21:58:58 +01:00
}
2015-03-19 01:59:44 +01:00
- (void)viewDidDisappear:(BOOL)animated {
2015-01-31 12:00:58 +01:00
_isVisible = NO;
}
2014-10-29 21:58:58 +01:00
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
#pragma mark - Initiliazers
- (IBAction)didSelectShow:(id)sender {
if (isGroupConversation) {
UIBarButtonItem *spaceEdge = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
spaceEdge.width = 40;
UIBarButtonItem *spaceMiddleIcons = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
spaceMiddleIcons.width = 61;
UIBarButtonItem *spaceMiddleWords = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
NSDictionary* buttonTextAttributes = @{NSFontAttributeName:[UIFont ows_regularFontWithSize:15.0f],
NSForegroundColorAttributeName:[UIColor ows_materialBlueColor]};
UIButton* groupUpdateButton = [[UIButton alloc] initWithFrame:CGRectMake(0,0,65,24)];
NSMutableAttributedString *updateTitle = [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"UPDATE_BUTTON_TITLE", @"")];
[updateTitle setAttributes:buttonTextAttributes range:NSMakeRange(0, [updateTitle length])];
[groupUpdateButton setAttributedTitle:updateTitle forState:UIControlStateNormal];
[groupUpdateButton addTarget:self action:@selector(updateGroup) forControlEvents:UIControlEventTouchUpInside];
[groupUpdateButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
2015-03-21 15:19:42 +01:00
[groupUpdateButton.titleLabel setAdjustsFontSizeToFitWidth:YES];
UIBarButtonItem *groupUpdateBarButton = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:self action:nil];
groupUpdateBarButton.customView = groupUpdateButton;
groupUpdateBarButton.customView.userInteractionEnabled = YES;
UIButton* groupLeaveButton = [[UIButton alloc] initWithFrame:CGRectMake(0,0,50,24)];
NSMutableAttributedString *leaveTitle = [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"LEAVE_BUTTON_TITLE", @"")];
[leaveTitle setAttributes:buttonTextAttributes range:NSMakeRange(0, [leaveTitle length])];
[groupLeaveButton setAttributedTitle:leaveTitle forState:UIControlStateNormal];
[groupLeaveButton addTarget:self action:@selector(leaveGroup) forControlEvents:UIControlEventTouchUpInside];
[groupLeaveButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
UIBarButtonItem *groupLeaveBarButton = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:self action:nil];
groupLeaveBarButton.customView = groupLeaveButton;
groupLeaveBarButton.customView.userInteractionEnabled = YES;
2015-03-21 15:19:42 +01:00
[groupLeaveButton.titleLabel setAdjustsFontSizeToFitWidth:YES];
UIButton* groupMembersButton = [[UIButton alloc] initWithFrame:CGRectMake(0,0,65,24)];
NSMutableAttributedString *membersTitle = [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"MEMBERS_BUTTON_TITLE", @"")];
[membersTitle setAttributes:buttonTextAttributes range:NSMakeRange(0, [membersTitle length])];
[groupMembersButton setAttributedTitle:membersTitle forState:UIControlStateNormal];
[groupMembersButton addTarget:self action:@selector(showGroupMembers) forControlEvents:UIControlEventTouchUpInside];
[groupMembersButton.titleLabel setTextAlignment:NSTextAlignmentCenter];
UIBarButtonItem *groupMembersBarButton = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:self action:nil];
groupMembersBarButton.customView = groupMembersButton;
groupMembersBarButton.customView.userInteractionEnabled = YES;
2015-03-21 15:19:42 +01:00
[groupMembersButton.titleLabel setAdjustsFontSizeToFitWidth:YES];
self.navController.dropDownToolbar.items =@[spaceEdge, groupUpdateBarButton, spaceMiddleWords, groupLeaveBarButton, spaceMiddleWords, groupMembersBarButton, spaceEdge];
for(UIButton *button in self.navController.dropDownToolbar.items) {
[button setTintColor:[UIColor ows_materialBlueColor]];
}
if(self.navController.isDropDownVisible){
[self.navController hideDropDown:sender];
}
else{
[self.navController showDropDown:sender];
}
// Can also toggle toolbar from current state
// [self.navController toggleToolbar:sender];
[self setNavigationTitle];
}
}
-(void) setNavigationTitle {
NSString* navTitle = self.thread.name;
if(isGroupConversation && [navTitle length]==0) {
navTitle = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"");
}
self.navController.activeNavigationBarTitle = nil;
self.title = navTitle;
}
-(void)initializeToolbars {
self.navController = (APNavigationController*)self.navigationController;
if([self canCall]) {
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[[UIImage imageNamed:@"btnPhone--white"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] style:UIBarButtonItemStylePlain target:self action:@selector(callAction)];
self.navigationItem.rightBarButtonItem.imageInsets = UIEdgeInsetsMake(0, -10, 0, 10);
} else if(!_thread.isGroupThread) {
self.navigationItem.rightBarButtonItem = nil;
}
[self hideInputIfNeeded];
[self setNavigationTitle];
}
- (void)initializeTitleLabelGestureRecognizer {
if(isGroupConversation) {
return;
}
for (UIView *view in self.navigationController.navigationBar.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UINavigationItemView")]) {
self.navView = view;
for (UIView *aView in self.navView.subviews) {
if ([aView isKindOfClass:[UILabel class]]) {
UILabel *label = (UILabel*)aView;
if ([label.text isEqualToString:self.title]) {
[self.navView setUserInteractionEnabled:YES];
[aView setUserInteractionEnabled:YES];
[aView addGestureRecognizer:_showFingerprintDisplay];
[aView addGestureRecognizer:_toggleContactPhoneDisplay];
return;
}
}
}
}
}
}
- (void)removeTitleLabelGestureRecognizer {
if(isGroupConversation) {
return;
}
for (UIView *aView in self.navView.subviews) {
if ([aView isKindOfClass:[UILabel class]]) {
UILabel *label = (UILabel*)aView;
if ([label.text isEqualToString:self.title]) {
[self.navView setUserInteractionEnabled:NO];
[aView setUserInteractionEnabled:NO];
[aView removeGestureRecognizer:_showFingerprintDisplay];
[aView removeGestureRecognizer:_toggleContactPhoneDisplay];
return;
}
}
}
}
-(void)initializeBubbles
{
JSQMessagesBubbleImageFactory *bubbleFactory = [[JSQMessagesBubbleImageFactory alloc] init];
self.outgoingBubbleImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor ows_materialBlueColor]];
self.incomingBubbleImageData = [bubbleFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]];
self.currentlyOutgoingBubbleImageData = [bubbleFactory outgoingMessageFailedBubbleImageWithColor:[UIColor ows_fadedBlueColor]];
self.outgoingMessageFailedImageData = [bubbleFactory outgoingMessageFailedBubbleImageWithColor:[UIColor grayColor]];
}
-(void)initializeCollectionViewLayout
{
if (self.collectionView){
[self.collectionView.collectionViewLayout setMessageBubbleFont:[UIFont ows_regularFontWithSize:15.0f]];
self.collectionView.showsVerticalScrollIndicator = NO;
self.collectionView.showsHorizontalScrollIndicator = NO;
[self updateLoadEarlierVisible];
self.collectionView.collectionViewLayout.incomingAvatarViewSize = CGSizeZero;
self.collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero;
}
}
2014-10-29 21:58:58 +01:00
#pragma mark - Fingerprints
-(void)showFingerprint
{
[self markAllMessagesAsRead];
[self performSegueWithIdentifier:kFingerprintSegueIdentifier sender:self];
2014-10-29 21:58:58 +01:00
}
2014-12-04 00:23:36 +01:00
-(void) toggleContactPhone {
_displayPhoneAsTitle = !_displayPhoneAsTitle;
if (!_thread.isGroupThread) {
Contact *contact = [[[Environment getCurrent] contactsManager] latestContactForPhoneNumber:[self phoneNumberForThread]];
if (!contact) {
if (!(SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(NSFoundationVersionNumber_iOS_9))) {
ABUnknownPersonViewController *view = [[ABUnknownPersonViewController alloc] init];
ABRecordRef aContact = ABPersonCreate();
CFErrorRef anError = NULL;
ABMultiValueRef phone = ABMultiValueCreateMutable(kABMultiStringPropertyType);
ABMultiValueAddValueAndLabel(phone, (__bridge CFTypeRef) [self phoneNumberForThread].toE164, kABPersonPhoneMainLabel, NULL);
ABRecordSetValue(aContact, kABPersonPhoneProperty, phone, &anError);
CFRelease(phone);
if (!anError && aContact) {
view.displayedPerson = aContact; // Assume person is already defined.
view.allowsAddingToAddressBook = YES;
[self.navigationController pushViewController:view animated:YES];
}
} else {
CNContactStore *contactStore = [Environment getCurrent].contactsManager.contactStore;
CNMutableContact *cncontact = [[CNMutableContact alloc] init];
cncontact.phoneNumbers = @[[CNLabeledValue labeledValueWithLabel:nil value:[CNPhoneNumber phoneNumberWithStringValue:[self phoneNumberForThread].toE164]]];
CNContactViewController *controller = [CNContactViewController viewControllerForUnknownContact:cncontact];
controller.allowsActions = NO;
controller.allowsEditing = YES;
controller.contactStore = contactStore;
[self.navigationController pushViewController:controller animated:YES];
// The "Add to existing contacts" is known to be destroying the view controller stack on iOS 9 http://stackoverflow.com/questions/32973254/cncontactviewcontroller-forunknowncontact-unusable-destroys-interface
// Warning the user
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"iOS 9 Bug" message:@"iOS 9 introduced a bug that prevents us from adding this number to an existing contact from the app. You can still create a new contact for this number or copy-paste it into an existing contact sheet." preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Continue" style:UIAlertActionStyleCancel handler:nil]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Copy number" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = [self phoneNumberForThread].toE164;
[controller.navigationController popViewControllerAnimated:YES];
}]];
[controller presentViewController:alertController animated:YES completion:nil];
}
}
}
if(_displayPhoneAsTitle) {
self.title = [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:[[self phoneNumberForThread] toE164]];
}
else {
[self setNavigationTitle];
}
}
-(void)showGroupMembers {
[self.navController hideDropDown:self];
[self performSegueWithIdentifier:kShowGroupMembersSegue sender:self];
}
#pragma mark - Calls
-(BOOL)isRedPhoneReachable
{
return [[Environment getCurrent].contactsManager isPhoneNumberRegisteredWithRedPhone:[self phoneNumberForThread]];
}
-(BOOL)isTextSecureReachable {
if(isGroupConversation) {
return YES;
}
else {
__block TSRecipient *recipient;
[self.editingDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
recipient = [TSRecipient recipientWithTextSecureIdentifier:[self phoneNumberForThread].toE164 withTransaction:transaction];
}];
return recipient?YES:NO;
}
}
-(PhoneNumber*)phoneNumberForThread
{
NSString * contactId = [(TSContactThread*)self.thread contactIdentifier];
return [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:contactId];
}
-(void)callAction
{
if ([self isRedPhoneReachable]) {
PhoneNumber *number = [self phoneNumberForThread];
Contact *contact = [[Environment.getCurrent contactsManager] latestContactForPhoneNumber:number];
[Environment.phoneManager initiateOutgoingCallToContact:contact atRemoteNumber:number];
} else {
DDLogWarn(@"Tried to initiate a call but contact has no RedPhone identifier");
}
}
-(BOOL) canCall {
return !isGroupConversation && [self isRedPhoneReachable] && ![((TSContactThread*)_thread).contactIdentifier isEqualToString:[SignalKeyingStorage.localNumber toE164]];
}
- (void)textViewDidChange:(UITextView *)textView {
if([textView.text length]>0) {
self.inputToolbar.contentView.rightBarButtonItem.enabled = YES;
}
else {
self.inputToolbar.contentView.rightBarButtonItem.enabled = NO;
}
}
2014-10-29 21:58:58 +01:00
#pragma mark - JSQMessagesViewController method overrides
- (void)didPressSendButton:(UIButton *)button
withMessageText:(NSString *)text
senderId:(NSString *)senderId
senderDisplayName:(NSString *)senderDisplayName
date:(NSDate *)date
{
2014-12-04 11:27:45 +01:00
if (text.length > 0) {
2014-10-29 21:58:58 +01:00
[JSQSystemSoundPlayer jsq_playMessageSentSound];
2014-12-21 12:52:42 +01:00
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:self.thread messageBody:text attachments:nil];
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
[[TSMessagesManager sharedManager] sendMessage:message inThread:self.thread success:nil failure:nil];
2014-10-29 21:58:58 +01:00
[self finishSendingMessage];
}
}
#pragma mark - JSQMessages CollectionView DataSource
- (id<JSQMessageData>)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath
{
2014-11-25 21:33:29 +01:00
return [self messageAtIndexPath:indexPath];
2014-10-29 21:58:58 +01:00
}
- (id<JSQMessageBubbleImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
{
2014-11-25 21:33:29 +01:00
id<JSQMessageData> message = [self messageAtIndexPath:indexPath];
2014-10-29 21:58:58 +01:00
if ([message.senderId isEqualToString:self.senderId]) {
switch (message.messageState) {
case TSOutgoingMessageStateUnsent:
return self.outgoingMessageFailedImageData;
case TSOutgoingMessageStateAttemptingOut:
return self.currentlyOutgoingBubbleImageData;
default:
return self.outgoingBubbleImageData;
}
2014-10-29 21:58:58 +01:00
}
2014-11-25 16:38:33 +01:00
return self.incomingBubbleImageData;
2014-10-29 21:58:58 +01:00
}
- (id<JSQMessageAvatarImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
{
return nil;
}
#pragma mark - UICollectionView DataSource
- (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
TSMessageAdapter * msg = [self messageAtIndexPath:indexPath];
switch (msg.messageType) {
case TSIncomingMessageAdapter:
return [self loadIncomingMessageCellForMessage:msg atIndexPath:indexPath];
case TSOutgoingMessageAdapter:
return [self loadOutgoingCellForMessage:msg atIndexPath:indexPath];
case TSCallAdapter:
return [self loadCallCellForCall:msg atIndexPath:indexPath];
case TSInfoMessageAdapter:
return [self loadInfoMessageCellForMessage:msg atIndexPath:indexPath];
case TSErrorMessageAdapter:
return [self loadErrorMessageCellForMessage:msg atIndexPath:indexPath];
default:
DDLogError(@"Something went wrong");
return nil;
}
}
#pragma mark - Loading message cells
-(JSQMessagesCollectionViewCell*)loadIncomingMessageCellForMessage:(id<JSQMessageData>)message atIndexPath:(NSIndexPath*)indexPath
{
JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath];
if (!message.isMediaMessage) {
cell.textView.textColor = [UIColor ows_blackColor];
2014-11-25 16:38:33 +01:00
cell.textView.linkTextAttributes = @{ NSForegroundColorAttributeName : cell.textView.textColor,
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) };
2014-10-29 21:58:58 +01:00
}
2014-11-25 16:38:33 +01:00
return cell;
}
-(JSQMessagesCollectionViewCell*)loadOutgoingCellForMessage:(id<JSQMessageData>)message atIndexPath:(NSIndexPath*)indexPath
{
JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath];
if (!message.isMediaMessage)
{
cell.textView.textColor = [UIColor whiteColor];
cell.textView.linkTextAttributes = @{ NSForegroundColorAttributeName : cell.textView.textColor,
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) };
}
return cell;
}
-(JSQCallCollectionViewCell*)loadCallCellForCall:(id<JSQMessageData>)call atIndexPath:(NSIndexPath*)indexPath
{
JSQCallCollectionViewCell *cell = (JSQCallCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath];
return cell;
}
-(JSQDisplayedMessageCollectionViewCell *)loadInfoMessageCellForMessage:(id<JSQMessageData>)message atIndexPath:(NSIndexPath*)indexPath
{
JSQDisplayedMessageCollectionViewCell * cell = (JSQDisplayedMessageCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath];
return cell;
}
-(JSQDisplayedMessageCollectionViewCell *)loadErrorMessageCellForMessage:(id<JSQMessageData>)message atIndexPath:(NSIndexPath*)indexPath
{
JSQDisplayedMessageCollectionViewCell * cell = (JSQDisplayedMessageCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath];
return cell;
2014-10-29 21:58:58 +01:00
}
#pragma mark - Adjusting cell label heights
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
{
2014-11-25 16:38:33 +01:00
if ([self showDateAtIndexPath:indexPath]) {
2014-10-29 21:58:58 +01:00
return kJSQMessagesCollectionViewCellLabelHeightDefault;
}
2014-10-29 21:58:58 +01:00
return 0.0f;
}
2014-11-25 16:38:33 +01:00
- (BOOL)showDateAtIndexPath:(NSIndexPath *)indexPath
2014-10-29 21:58:58 +01:00
{
2014-11-25 16:38:33 +01:00
BOOL showDate = NO;
if (indexPath.row == 0) {
showDate = YES;
2014-10-29 21:58:58 +01:00
}
2014-11-25 16:38:33 +01:00
else {
2014-11-25 21:33:29 +01:00
TSMessageAdapter *currentMessage = [self messageAtIndexPath:indexPath];
2014-11-25 21:33:29 +01:00
TSMessageAdapter *previousMessage = [self messageAtIndexPath:[NSIndexPath indexPathForItem:indexPath.row-1 inSection:indexPath.section]];
2014-11-25 16:38:33 +01:00
NSTimeInterval timeDifference = [currentMessage.date timeIntervalSinceDate:previousMessage.date];
if (timeDifference > kTSMessageSentDateShowTimeInterval) {
showDate = YES;
2014-10-29 21:58:58 +01:00
}
}
2014-11-25 16:38:33 +01:00
return showDate;
2014-10-29 21:58:58 +01:00
}
-(NSAttributedString*)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath
{
if ([self showDateAtIndexPath:indexPath]) {
TSMessageAdapter *currentMessage = [self messageAtIndexPath:indexPath];
return [[JSQMessagesTimestampFormatter sharedFormatter] attributedTimestampForDate:currentMessage.date];
}
return nil;
}
-(BOOL)shouldShowMessageStatusAtIndexPath:(NSIndexPath*)indexPath
{
TSMessageAdapter *currentMessage = [self messageAtIndexPath:indexPath];
// If message failed, say that message should be tapped to retry;
if (currentMessage.messageType == TSOutgoingMessageAdapter
&& currentMessage.messageState == TSOutgoingMessageStateUnsent) {
return YES;
}
if([self.thread isKindOfClass:[TSGroupThread class]]) {
return currentMessage.messageType == TSIncomingMessageAdapter;
} else {
if (indexPath.item == [self.collectionView numberOfItemsInSection:indexPath.section]-1) {
return [self isMessageOutgoingAndDelivered:currentMessage];
}
if (![self isMessageOutgoingAndDelivered:currentMessage]) {
return NO;
}
TSMessageAdapter *nextMessage = [self nextOutgoingMessage:indexPath];
return ![self isMessageOutgoingAndDelivered:nextMessage];
}
2014-12-04 15:01:05 +01:00
}
-(TSMessageAdapter*)nextOutgoingMessage:(NSIndexPath*)indexPath
{
TSMessageAdapter * nextMessage = [self messageAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:indexPath.section]];
2014-12-04 15:01:05 +01:00
int i = 1;
2014-12-04 15:01:05 +01:00
while (indexPath.item+i < [self.collectionView numberOfItemsInSection:indexPath.section]-1 && ![self isMessageOutgoingAndDelivered:nextMessage]) {
i++;
nextMessage = [self messageAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row+i inSection:indexPath.section]];
}
2014-12-04 15:01:05 +01:00
return nextMessage;
}
-(BOOL)isMessageOutgoingAndDelivered:(TSMessageAdapter*)message
{
return message.messageType == TSOutgoingMessageAdapter && message.messageState == TSOutgoingMessageStateDelivered;
}
-(NSAttributedString*)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath {
TSMessageAdapter *msg = [self messageAtIndexPath:indexPath];
NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
textAttachment.bounds = CGRectMake(0, 0, 11.0f, 10.0f);
if ([self shouldShowMessageStatusAtIndexPath:indexPath]) {
if (msg.messageType == TSOutgoingMessageAdapter
&& msg.messageState == TSOutgoingMessageStateUnsent) {
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:NSLocalizedString(@"FAILED_SENDING_TEXT", nil)];
[attrStr appendAttributedString:[NSAttributedString attributedStringWithAttachment:textAttachment]];
return attrStr;
}
if([self.thread isKindOfClass:[TSGroupThread class]]) {
NSString *name = [[Environment getCurrent].contactsManager nameStringForPhoneIdentifier:msg.senderId];
name = name ? name : msg.senderId;
if (!name) {
name = @"";
}
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:name];
[attrStr appendAttributedString:[NSAttributedString attributedStringWithAttachment:textAttachment]];
return attrStr;
}
else {
_lastDeliveredMessageIndexPath = indexPath;
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:NSLocalizedString(@"DELIVERED_MESSAGE_TEXT", @"")];
[attrStr appendAttributedString:[NSAttributedString attributedStringWithAttachment:textAttachment]];
return attrStr;
}
}
return nil;
}
2014-10-29 21:58:58 +01:00
- (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView
layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
{
2015-03-19 01:59:44 +01:00
if ([self shouldShowMessageStatusAtIndexPath:indexPath]) {
return 16.0f;
}
return 0.0f;
2014-10-29 21:58:58 +01:00
}
#pragma mark - Actions
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapMessageBubbleAtIndexPath:(NSIndexPath *)indexPath
{
2014-12-11 00:05:41 +01:00
TSMessageAdapter *messageItem = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:indexPath];
TSInteraction *interaction = [self interactionAtIndexPath:indexPath];
2014-12-11 00:05:41 +01:00
switch (messageItem.messageType) {
case TSOutgoingMessageAdapter:
if (messageItem.messageState == TSOutgoingMessageStateUnsent) {
[self handleUnsentMessageTap:(TSOutgoingMessage*)interaction];
}
case TSIncomingMessageAdapter:{
2014-12-11 00:05:41 +01:00
BOOL isMediaMessage = [messageItem isMediaMessage];
2014-12-11 00:05:41 +01:00
if (isMediaMessage) {
if([[messageItem media] isKindOfClass:[TSPhotoAdapter class]]) {
TSPhotoAdapter* messageMedia = (TSPhotoAdapter*)[messageItem media];
if ([messageMedia isImage]) {
tappedImage = ((UIImageView*)[messageMedia mediaView]).image;
CGRect convertedRect = [self.collectionView convertRect:[collectionView cellForItemAtIndexPath:indexPath].frame toView:nil];
__block TSAttachment *attachment = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
attachment = [TSAttachment fetchObjectWithUniqueID:messageMedia.attachmentId transaction:transaction];
}];
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream *attStream = (TSAttachmentStream*)attachment;
FullImageViewController * vc = [[FullImageViewController alloc] initWithAttachment:attStream fromRect:convertedRect forInteraction:[self interactionAtIndexPath:indexPath]];
2015-01-25 02:40:51 +01:00
[vc presentFromViewController:self.navigationController];
}
} else {
DDLogWarn(@"Currently unsupported");
}
}
else if([[messageItem media] isKindOfClass:[TSVideoAttachmentAdapter class]]){
// fileurl disappeared should look up in db as before. will do refactor
// full screen, check this setup with a .mov
TSVideoAttachmentAdapter* messageMedia = (TSVideoAttachmentAdapter*)[messageItem media];
2015-01-25 00:48:40 +01:00
_currentMediaAdapter = messageMedia;
__block TSAttachment *attachment = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
attachment = [TSAttachment fetchObjectWithUniqueID:messageMedia.attachmentId transaction:transaction];
}];
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream *attStream = (TSAttachmentStream*)attachment;
NSFileManager *fileManager = [NSFileManager defaultManager];
if([messageMedia isVideo]) {
if ([fileManager fileExistsAtPath:[attStream.mediaURL path]]) {
[self dismissKeyBoard];
_videoPlayer = [[MPMoviePlayerController alloc] initWithContentURL:attStream.mediaURL];
[_videoPlayer prepareToPlay];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayBackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object: _videoPlayer];
_videoPlayer.controlStyle = MPMovieControlStyleDefault;
_videoPlayer.shouldAutoplay = YES;
[self.view addSubview: _videoPlayer.view];
[_videoPlayer setFullscreen:YES animated:YES];
}
} else if([messageMedia isAudio]){
if (messageMedia.isAudioPlaying) {
// if you had started playing an audio msg and now you're tapping it to pause
messageMedia.isAudioPlaying = NO;
[_audioPlayer pause];
messageMedia.isPaused = YES;
[_audioPlayerPoller invalidate];
double current = [_audioPlayer currentTime]/[_audioPlayer duration];
[messageMedia setAudioProgressFromFloat:(float)current];
[messageMedia setAudioIconToPlay];
} else {
BOOL isResuming = NO;
[_audioPlayerPoller invalidate];
2015-01-25 00:48:40 +01:00
// loop through all the other bubbles and set their isPlaying to false
NSInteger num_bubbles = [self collectionView:collectionView numberOfItemsInSection:0];
for (NSInteger i=0; i<num_bubbles; i++) {
NSIndexPath *index_path = [NSIndexPath indexPathForRow:i inSection:0];
TSMessageAdapter *msgAdapter = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:index_path];
if (msgAdapter.messageType == TSIncomingMessageAdapter && msgAdapter.isMediaMessage) {
TSVideoAttachmentAdapter* msgMedia = (TSVideoAttachmentAdapter*)[msgAdapter media];
if ([msgMedia isAudio]) {
if (msgMedia == messageMedia && messageMedia.isPaused) {
isResuming = YES;
} else {
msgMedia.isAudioPlaying = NO;
msgMedia.isPaused = NO;
[msgMedia setAudioIconToPlay];
[msgMedia setAudioProgressFromFloat:0];
2015-01-25 00:48:40 +01:00
[msgMedia resetAudioDuration];
}
}
}
}
if (isResuming) {
// if you had paused an audio msg and now you're tapping to resume
[_audioPlayer prepareToPlay];
[_audioPlayer play];
[messageMedia setAudioIconToPause];
messageMedia.isAudioPlaying = YES;
messageMedia.isPaused = NO;
2015-01-25 00:48:40 +01:00
_audioPlayerPoller = [NSTimer scheduledTimerWithTimeInterval:.05 target:self selector:@selector(audioPlayerUpdated:) userInfo:@{@"adapter": messageMedia} repeats:YES];
} else {
// if you are tapping an audio msg for the first time to play
messageMedia.isAudioPlaying = YES;
NSError *error;
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:attStream.mediaURL error:&error];
2015-01-25 00:48:40 +01:00
if (error) {
DDLogError(@"error: %@", error);
2015-01-25 00:48:40 +01:00
}
[_audioPlayer prepareToPlay];
[_audioPlayer play];
[messageMedia setAudioIconToPause];
_audioPlayer.delegate = self;
2015-01-25 00:48:40 +01:00
_audioPlayerPoller = [NSTimer scheduledTimerWithTimeInterval:.05 target:self selector:@selector(audioPlayerUpdated:) userInfo:@{@"adapter": messageMedia} repeats:YES];
}
}
}
}
2014-12-11 00:05:41 +01:00
}
}
}
break;
2014-12-11 00:05:41 +01:00
case TSErrorMessageAdapter:
[self handleErrorMessageTap:(TSErrorMessage*)interaction];
break;
case TSInfoMessageAdapter:
2015-02-19 01:04:32 +01:00
[self handleWarningTap:interaction];
2014-12-11 00:05:41 +01:00
break;
case TSCallAdapter:
break;
2014-12-11 00:05:41 +01:00
default:
break;
}
2014-12-11 00:05:41 +01:00
}
2015-02-19 01:04:32 +01:00
- (void)handleWarningTap:(TSInteraction*)interaction {
if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *message = (TSIncomingMessage*) interaction;
for (NSString *attachmentId in message.attachments) {
__block TSAttachment *attachment;
[self.editingDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
}];
if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
TSAttachmentPointer *pointer = (TSAttachmentPointer*)attachment;
if (!pointer.isDownloading) {
[[TSMessagesManager sharedManager] retrieveAttachment:pointer messageId:message.uniqueId];
}
}
}
}
}
-(void)moviePlayBackDidFinish:(id)sender {
DDLogDebug(@"playback finished");
}
-(void)collectionView:(JSQMessagesCollectionView *)collectionView header:(JSQMessagesLoadEarlierHeaderView *)headerView didTapLoadEarlierMessagesButton:(UIButton *)sender
{
if ([self shouldShowLoadEarlierMessages]) {
self.page++;
}
NSInteger item = (NSInteger)[self scrollToItem];
[self updateRangeOptionsForPage:self.page];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self.messageMappings updateWithTransaction:transaction];
}];
[self updateLayoutForEarlierMessagesWithOffset:item];
}
-(BOOL)shouldShowLoadEarlierMessages
{
__block BOOL show = YES;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
show = [self.messageMappings numberOfItemsInGroup:self.thread.uniqueId] < [[transaction ext:TSMessageDatabaseViewExtensionName] numberOfItemsInGroup:self.thread.uniqueId];
}];
return show;
}
-(NSUInteger)scrollToItem
{
__block NSUInteger item = kYapDatabaseRangeLength*(self.page+1) - [self.messageMappings numberOfItemsInGroup:self.thread.uniqueId];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSUInteger numberOfVisibleMessages = [self.messageMappings numberOfItemsInGroup:self.thread.uniqueId] ;
NSUInteger numberOfTotalMessages = [[transaction ext:TSMessageDatabaseViewExtensionName] numberOfItemsInGroup:self.thread.uniqueId] ;
NSUInteger numberOfMessagesToLoad = numberOfTotalMessages - numberOfVisibleMessages ;
BOOL canLoadFullRange = numberOfMessagesToLoad >= kYapDatabaseRangeLength;
if (!canLoadFullRange) {
item = numberOfMessagesToLoad;
}
}];
return item == 0 ? item : item - 1;
}
-(void)updateLoadEarlierVisible
{
[self setShowLoadEarlierMessagesHeader:[self shouldShowLoadEarlierMessages]];
}
-(void)updateLayoutForEarlierMessagesWithOffset:(NSInteger)offset
{
[self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
[self.collectionView reloadData];
[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:offset inSection:0] atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
[self updateLoadEarlierVisible];
}
-(void)updateRangeOptionsForPage:(NSUInteger)page
{
YapDatabaseViewRangeOptions *rangeOptions = [YapDatabaseViewRangeOptions flexibleRangeWithLength:kYapDatabaseRangeLength*(page+1) offset:0 from:YapDatabaseViewEnd];
rangeOptions.maxLength = kYapDatabaseRangeMaxLength;
rangeOptions.minLength = kYapDatabaseRangeMinLength;
[self.messageMappings setRangeOptions:rangeOptions forGroup:self.thread.uniqueId];
}
2014-12-11 00:05:41 +01:00
#pragma mark Bubble User Actions
- (void)handleUnsentMessageTap:(TSOutgoingMessage*)message{
[self dismissKeyBoard];
[DJWActionSheet showInView:self.parentViewController.view withTitle:nil cancelButtonTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"") destructiveButtonTitle:NSLocalizedString(@"TXT_DELETE_TITLE", @"") otherButtonTitles:@[NSLocalizedString(@"SEND_AGAIN_BUTTON", @"")] tapBlock:^(DJWActionSheet *actionSheet, NSInteger tappedButtonIndex) {
2014-12-11 00:05:41 +01:00
if (tappedButtonIndex == actionSheet.cancelButtonIndex) {
DDLogDebug(@"User Cancelled");
2014-12-11 00:05:41 +01:00
} else if (tappedButtonIndex == actionSheet.destructiveButtonIndex) {
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){
[message removeWithTransaction:transaction];
}];
}else {
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
[[TSMessagesManager sharedManager] sendMessage:message inThread:self.thread success:nil failure:nil];
2014-12-11 00:05:41 +01:00
[self finishSendingMessage];
}
}];
}
- (void)deleteMessageAtIndexPath:(NSIndexPath*)indexPath {
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSInteraction *interaction = [self interactionAtIndexPath:indexPath];
[[TSAdapterCacheManager sharedManager] clearCacheEntryForInteractionId:interaction.uniqueId];
[interaction removeWithTransaction:transaction];
}];
}
- (void)handleErrorMessageTap:(TSErrorMessage*)message {
if ([message isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]) {
TSInvalidIdentityKeyErrorMessage *errorMessage = (TSInvalidIdentityKeyErrorMessage*)message;
NSString *newKeyFingerprint = [errorMessage newIdentityKey];
NSString *keyOwner;
if ([message isKindOfClass:[TSInvalidIdentityKeySendingErrorMessage class]]) {
TSInvalidIdentityKeySendingErrorMessage *m = (TSInvalidIdentityKeySendingErrorMessage*)message;
keyOwner = [[[Environment getCurrent] contactsManager] nameStringForPhoneIdentifier:m.recipientId];
} else {
keyOwner = [self.thread name];
}
NSString *messageString = [NSString stringWithFormat:NSLocalizedString(@"ACCEPT_IDENTITYKEY_QUESTION", @""), keyOwner, newKeyFingerprint];
NSArray *actions = @[NSLocalizedString(@"ACCEPT_IDENTITYKEY_BUTTON", @""), NSLocalizedString(@"COPY_IDENTITYKEY_BUTTON", @"")];
[self dismissKeyBoard];
[DJWActionSheet showInView:self.parentViewController.view withTitle:messageString cancelButtonTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"") destructiveButtonTitle:NSLocalizedString(@"TXT_DELETE_TITLE", @"") otherButtonTitles:actions tapBlock:^(DJWActionSheet *actionSheet, NSInteger tappedButtonIndex) {
if (tappedButtonIndex == actionSheet.cancelButtonIndex) {
DDLogDebug(@"User Cancelled");
} else if (tappedButtonIndex == actionSheet.destructiveButtonIndex) {
2014-12-11 00:05:41 +01:00
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){
[message removeWithTransaction:transaction];
}];
2014-12-11 00:05:41 +01:00
} else {
switch (tappedButtonIndex) {
case 0:
[errorMessage acceptNewIdentityKey];
2014-12-11 00:05:41 +01:00
break;
case 1:
[[UIPasteboard generalPasteboard] setString:newKeyFingerprint];
break;
default:
break;
}
}
}];
2014-10-29 21:58:58 +01:00
}
}
#pragma mark - Navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:kFingerprintSegueIdentifier]){
2014-12-04 00:23:36 +01:00
FingerprintViewController *vc = [segue destinationViewController];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[vc configWithThread:self.thread];
}];
2014-10-29 21:58:58 +01:00
}
else if ([segue.identifier isEqualToString:kUpdateGroupSegueIdentifier]) {
NewGroupViewController *vc = [segue destinationViewController];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[vc configWithThread:(TSGroupThread*)self.thread];
}];
}
else if([segue.identifier isEqualToString:kShowGroupMembersSegue]) {
ShowGroupMembersViewController *vc = [segue destinationViewController];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[vc configWithThread:(TSGroupThread*)self.thread];
}];
}
2014-10-29 21:58:58 +01:00
}
#pragma mark - UIImagePickerController
/*
* Presenting UIImagePickerController
*/
2015-03-19 01:59:44 +01:00
- (void)takePictureOrVideo {
2014-10-29 21:58:58 +01:00
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.allowsEditing = NO;
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
2014-10-29 21:58:58 +01:00
if ([UIImagePickerController isSourceTypeAvailable:
UIImagePickerControllerSourceTypeCamera]) {
picker.mediaTypes = @[(NSString*)kUTTypeImage,(NSString*)kUTTypeMovie];
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
2014-10-29 21:58:58 +01:00
}
2014-10-29 21:58:58 +01:00
}
2015-03-19 01:59:44 +01:00
- (void)chooseFromLibrary {
2014-10-29 21:58:58 +01:00
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
NSArray* photoOrVideoTypeArray = [[NSArray alloc] initWithObjects:(NSString *)kUTTypeImage,(NSString *)kUTTypeMovie, (NSString*)kUTTypeVideo, nil];
picker.mediaTypes = photoOrVideoTypeArray;
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
2014-10-29 21:58:58 +01:00
}
}
/*
* Dismissing UIImagePickerController
*/
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
2015-01-31 09:38:52 +01:00
[UIUtil modalCompletionBlock]();
2014-10-29 21:58:58 +01:00
[self dismissViewControllerAnimated:YES completion:nil];
}
2015-03-19 01:59:44 +01:00
- (void)resetFrame {
2015-01-31 09:38:52 +01:00
// fixes bug on frame being off after this selection
CGRect frame = [UIScreen mainScreen].applicationFrame;
self.view.frame = frame;
}
2014-10-29 21:58:58 +01:00
/*
* Fetching data from UIImagePickerController
2014-10-29 21:58:58 +01:00
*/
2015-03-19 01:59:44 +01:00
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
2015-01-31 09:38:52 +01:00
[UIUtil modalCompletionBlock]();
[self resetFrame];
2014-10-29 21:58:58 +01:00
NSString *mediaType = [info objectForKey: UIImagePickerControllerMediaType];
if (CFStringCompare ((__bridge_retained CFStringRef)mediaType, kUTTypeMovie, 0) == kCFCompareEqualTo) {
NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL];
[self sendQualityAdjustedAttachment:videoURL];
}
else {
UIImage *picture_camera = [[info objectForKey:UIImagePickerControllerOriginalImage] normalizedImage];
if(picture_camera) {
DDLogVerbose(@"Sending picture attachement ...");
[self sendMessageAttachment:[self qualityAdjustedAttachmentForImage:picture_camera] ofType:@"image/jpeg"];
}
2014-10-29 21:58:58 +01:00
}
}
2015-03-19 01:59:44 +01:00
- (void) sendMessageAttachment:(NSData*)attachmentData ofType:(NSString*)attachmentType {
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:self.thread messageBody:nil attachments:[NSMutableArray array]];
[self dismissViewControllerAnimated:YES completion:^{
[[TSMessagesManager sharedManager] sendAttachment:attachmentData contentType:attachmentType inMessage:message thread:self.thread];
}];
}
2015-04-25 16:59:32 +02:00
- (NSURL*)videoTempFolder {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
basePath = [basePath stringByAppendingPathComponent:@"videos"];
if (![[NSFileManager defaultManager] fileExistsAtPath:basePath]) {
[[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:YES attributes:nil error:nil];
}
2015-04-25 16:59:32 +02:00
return [NSURL fileURLWithPath:basePath];
}
-(void)sendQualityAdjustedAttachment:(NSURL*)movieURL {
AVAsset *video = [AVAsset assetWithURL:movieURL];
AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:video presetName:AVAssetExportPresetMediumQuality];
exportSession.shouldOptimizeForNetworkUse = YES;
exportSession.outputFileType = AVFileTypeMPEG4;
double currentTime = [[NSDate date] timeIntervalSince1970];
NSString *strImageName = [NSString stringWithFormat:@"%f",currentTime];
2015-04-25 16:59:32 +02:00
NSURL *compressedVideoUrl = [[self videoTempFolder] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.mp4",strImageName]];
exportSession.outputURL = compressedVideoUrl;
[exportSession exportAsynchronouslyWithCompletionHandler:^{
2015-04-25 16:59:32 +02:00
NSError *error;
2015-03-19 01:59:44 +01:00
[self sendMessageAttachment:[NSData dataWithContentsOfURL:compressedVideoUrl] ofType:@"video/mp4"];
2015-04-25 16:59:32 +02:00
[[NSFileManager defaultManager] removeItemAtURL:compressedVideoUrl error:&error];
if (error) {
DDLogWarn(@"Failed to remove cached video file: %@", error.debugDescription);
}
}];
2014-11-25 16:38:33 +01:00
}
2015-03-19 01:59:44 +01:00
- (NSData*)qualityAdjustedAttachmentForImage:(UIImage*)image {
2014-12-26 21:17:43 +01:00
return UIImageJPEGRepresentation([self adjustedImageSizedForSending:image], [self compressionRate]);
}
2015-03-19 01:59:44 +01:00
- (UIImage*)adjustedImageSizedForSending:(UIImage*)image {
2014-12-26 21:17:43 +01:00
CGFloat correctedWidth;
switch ([Environment.preferences imageUploadQuality]) {
case TSImageQualityUncropped:
return image;
2014-12-26 21:17:43 +01:00
case TSImageQualityHigh:
correctedWidth = 2048;
break;
case TSImageQualityMedium:
correctedWidth = 1024;
break;
case TSImageQualityLow:
correctedWidth = 512;
break;
default:
break;
}
2014-12-26 21:17:43 +01:00
return [self imageScaled:image toMaxSize:correctedWidth];
}
2015-03-19 01:59:44 +01:00
- (UIImage*)imageScaled:(UIImage *)image toMaxSize:(CGFloat)size {
2014-12-26 21:17:43 +01:00
CGFloat scaleFactor;
CGFloat aspectRatio = image.size.height / image.size.width;
2014-12-26 21:17:43 +01:00
if( aspectRatio > 1 ) {
scaleFactor = size / image.size.width;
}
else {
scaleFactor = size / image.size.height;
}
2014-12-26 21:17:43 +01:00
CGSize newSize = CGSizeMake(image.size.width * scaleFactor, image.size.height * scaleFactor);
2014-12-26 21:17:43 +01:00
UIGraphicsBeginImageContext(newSize);
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage* updatedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
2014-12-26 21:17:43 +01:00
return updatedImage;
}
2015-03-19 01:59:44 +01:00
- (CGFloat)compressionRate {
2014-12-26 21:17:43 +01:00
switch ([Environment.preferences imageUploadQuality]) {
case TSImageQualityUncropped:
return 1;
2014-12-26 21:17:43 +01:00
case TSImageQualityHigh:
return 0.9f;
case TSImageQualityMedium:
return 0.5f;
case TSImageQualityLow:
return 0.3f;
default:
break;
}
}
2014-11-25 16:38:33 +01:00
#pragma mark Storage access
- (YapDatabaseConnection*)uiDatabaseConnection {
2014-11-25 16:38:33 +01:00
NSAssert([NSThread isMainThread], @"Must access uiDatabaseConnection on main thread!");
if (!_uiDatabaseConnection) {
_uiDatabaseConnection = [[TSStorageManager sharedManager] newDatabaseConnection];
[_uiDatabaseConnection beginLongLivedReadTransaction];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(yapDatabaseModified:)
name:YapDatabaseModifiedNotification
object:nil];
}
return _uiDatabaseConnection;
}
- (YapDatabaseConnection*)editingDatabaseConnection {
if (!_editingDatabaseConnection) {
_editingDatabaseConnection = [[TSStorageManager sharedManager] newDatabaseConnection];
}
return _editingDatabaseConnection;
}
2015-03-19 01:59:44 +01:00
- (void)yapDatabaseModified:(NSNotification *)notification {
[self updateBackButtonAsync];
if(isGroupConversation) {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
TSGroupThread* gThread = (TSGroupThread*)self.thread;
self.thread = [TSGroupThread threadWithGroupModel:gThread.groupModel transaction:transaction];
}];
}
2014-11-25 16:38:33 +01:00
NSArray *notifications = [self.uiDatabaseConnection beginLongLivedReadTransaction];
2015-01-31 12:00:58 +01:00
2015-03-19 01:59:44 +01:00
if ( ![[self.uiDatabaseConnection ext:TSMessageDatabaseViewExtensionName] hasChangesForNotifications:notifications]) {
2015-01-31 12:00:58 +01:00
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
[self.messageMappings updateWithTransaction:transaction];
}];
return;
}
2015-03-19 01:59:44 +01:00
if (!_isVisible) {
2015-01-31 12:00:58 +01:00
// Since we moved our databaseConnection to a new commit,
// we need to update the mappings too.
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
[self.messageMappings updateWithTransaction:transaction];
}];
return;
}
2014-11-25 16:38:33 +01:00
NSArray *messageRowChanges = nil;
2015-01-31 12:00:58 +01:00
NSArray *sectionChanges = nil;
[[self.uiDatabaseConnection ext:TSMessageDatabaseViewExtensionName] getSectionChanges:&sectionChanges
2014-11-25 16:38:33 +01:00
rowChanges:&messageRowChanges
forNotifications:notifications
withMappings:self.messageMappings];
2015-01-05 16:31:00 +01:00
__block BOOL scrollToBottom = NO;
2015-01-31 12:00:58 +01:00
2015-03-19 01:59:44 +01:00
if ([sectionChanges count] == 0 & [messageRowChanges count] == 0) {
return;
}
2015-01-31 12:00:58 +01:00
[self.collectionView performBatchUpdates:^{
2015-03-19 01:59:44 +01:00
for (YapDatabaseViewRowChange *rowChange in messageRowChanges) {
switch (rowChange.type) {
case YapDatabaseViewChangeDelete :
{
[self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath ]];
break;
}
case YapDatabaseViewChangeInsert :
{
TSInteraction * interaction = [self interactionAtIndexPath:rowChange.newIndexPath];
[[TSAdapterCacheManager sharedManager] cacheAdapter:[TSMessageAdapter messageViewDataWithInteraction:interaction inThread:self.thread] forInteractionId:interaction.uniqueId];
[self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]];
2015-01-05 16:31:00 +01:00
scrollToBottom = YES;
break;
}
case YapDatabaseViewChangeMove :
{
[self.collectionView deleteItemsAtIndexPaths:@[ rowChange.indexPath ]];
[self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]];
break;
}
case YapDatabaseViewChangeUpdate :
{
NSMutableArray *rowsToUpdate = [@[rowChange.indexPath] mutableCopy];
if (_lastDeliveredMessageIndexPath) {
[rowsToUpdate addObject:_lastDeliveredMessageIndexPath];
}
for (NSIndexPath* indexPath in rowsToUpdate) {
TSInteraction * interaction = [self interactionAtIndexPath:indexPath];
[[TSAdapterCacheManager sharedManager] cacheAdapter:[TSMessageAdapter messageViewDataWithInteraction:interaction inThread:self.thread] forInteractionId:interaction.uniqueId];
}
[self.collectionView reloadItemsAtIndexPaths:rowsToUpdate];
2015-01-05 16:31:00 +01:00
scrollToBottom = YES;
break;
}
}
}
} completion:^(BOOL success) {
if (!success) {
[self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
[self.collectionView reloadData];
}
2015-01-05 16:31:00 +01:00
if (scrollToBottom) {
[self scrollToBottomAnimated:YES];
}
}];
2014-11-25 16:38:33 +01:00
}
#pragma mark - UICollectionView DataSource
2015-03-19 01:59:44 +01:00
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
NSInteger numberOfMessages = (NSInteger)[self.messageMappings numberOfItemsInSection:(NSUInteger)section];
2014-11-25 16:38:33 +01:00
return numberOfMessages;
2014-10-29 21:58:58 +01:00
}
2014-11-25 16:38:33 +01:00
2014-12-11 00:05:41 +01:00
- (TSInteraction*)interactionAtIndexPath:(NSIndexPath*)indexPath {
2014-11-25 16:38:33 +01:00
__block TSInteraction *message = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
NSParameterAssert(viewTransaction != nil);
NSParameterAssert(self.messageMappings != nil);
NSParameterAssert(indexPath != nil);
NSUInteger row = (NSUInteger)indexPath.row;
NSUInteger section = (NSUInteger)indexPath.section;
2014-11-25 16:38:33 +01:00
NSUInteger numberOfItemsInSection = [self.messageMappings numberOfItemsInSection:section];
2014-11-25 16:38:33 +01:00
NSAssert(row < numberOfItemsInSection, @"Cannot fetch message because row %d is >= numberOfItemsInSection %d", (int)row, (int)numberOfItemsInSection);
2014-11-25 16:38:33 +01:00
message = [viewTransaction objectAtRow:row inSection:section withMappings:self.messageMappings];
NSParameterAssert(message != nil);
}];
2014-12-11 00:05:41 +01:00
return message;
}
- (TSMessageAdapter*)messageAtIndexPath:(NSIndexPath *)indexPath {
TSInteraction *interaction = [self interactionAtIndexPath:indexPath];
TSAdapterCacheManager * manager = [TSAdapterCacheManager sharedManager];
if (![manager containsCacheEntryForInteractionId:interaction.uniqueId]) {
[manager cacheAdapter:[TSMessageAdapter messageViewDataWithInteraction:interaction inThread:self.thread] forInteractionId:interaction.uniqueId];
}
return [manager adapterForInteractionId:interaction.uniqueId];
2014-11-25 16:38:33 +01:00
}
#pragma mark group action view
#pragma mark - Audio
-(void)recordAudio {
// Define the recorder setting
NSArray *pathComponents = [NSArray arrayWithObjects:
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject],
[NSString stringWithFormat:@"%lld.m4a",[NSDate ows_millisecondTimeStamp]],
nil];
NSURL *outputFileURL = [NSURL fileURLWithPathComponents:pathComponents];
// Setup audio session
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc] init];
[recordSetting setValue:[NSNumber numberWithInt:kAudioFormatMPEG4AAC] forKey:AVFormatIDKey];
[recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
[recordSetting setValue:[NSNumber numberWithInt: 2] forKey:AVNumberOfChannelsKey];
// Initiate and prepare the recorder
_audioRecorder = [[AVAudioRecorder alloc] initWithURL:outputFileURL settings:recordSetting error:NULL];
_audioRecorder.delegate = self;
_audioRecorder.meteringEnabled = YES;
[_audioRecorder prepareToRecord];
}
- (void)audioPlayerUpdated:(NSTimer*)timer {
double current = [_audioPlayer currentTime]/[_audioPlayer duration];
2015-01-25 00:48:40 +01:00
double interval = [_audioPlayer duration] - [_audioPlayer currentTime];
[_currentMediaAdapter setDurationOfAudio:interval];
[_currentMediaAdapter setAudioProgressFromFloat:(float)current];
}
- (void) audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
[_audioPlayerPoller invalidate];
2015-01-25 00:48:40 +01:00
[_currentMediaAdapter setAudioProgressFromFloat:0];
[_currentMediaAdapter setDurationOfAudio:_audioPlayer.duration];
[_currentMediaAdapter setAudioIconToPlay];
}
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder
successfully:(BOOL)flag {
if(flag) {
[self sendMessageAttachment:[NSData dataWithContentsOfURL:recorder.url] ofType:@"audio/m4a"];
}
}
2014-11-25 16:38:33 +01:00
#pragma mark Accessory View
2015-03-19 01:59:44 +01:00
- (void)didPressAccessoryButton:(UIButton *)sender {
[self dismissKeyBoard];
2014-11-25 16:38:33 +01:00
UIView *presenter = self.parentViewController.view;
2014-11-25 16:38:33 +01:00
[DJWActionSheet showInView:presenter
withTitle:nil
cancelButtonTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"")
2014-11-25 16:38:33 +01:00
destructiveButtonTitle:nil
otherButtonTitles:@[NSLocalizedString(@"TAKE_MEDIA_BUTTON", @""), NSLocalizedString(@"CHOOSE_MEDIA_BUTTON", @"")]//,@"Record audio"]
2014-11-25 16:38:33 +01:00
tapBlock:^(DJWActionSheet *actionSheet, NSInteger tappedButtonIndex) {
if (tappedButtonIndex == actionSheet.cancelButtonIndex) {
DDLogVerbose(@"User Cancelled");
2014-11-25 16:38:33 +01:00
} else if (tappedButtonIndex == actionSheet.destructiveButtonIndex) {
DDLogVerbose(@"Destructive button tapped");
2015-03-19 01:59:44 +01:00
} else {
2014-11-25 16:38:33 +01:00
switch (tappedButtonIndex) {
case 0:
[self takePictureOrVideo];
break;
case 1:
[self chooseFromLibrary];
2014-11-25 16:38:33 +01:00
break;
case 2:
[self recordAudio];
break;
2014-11-25 16:38:33 +01:00
default:
break;
}
}
}];
}
- (void)markAllMessagesAsRead {
[self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self.thread markAllAsReadWithTransaction:transaction];
}];
}
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(delete:)) {
return YES;
}
return [super collectionView:collectionView canPerformAction:action forItemAtIndexPath:indexPath withSender:sender];
}
2015-03-19 01:59:44 +01:00
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (action == @selector(delete:)) {
[self deleteMessageAtIndexPath:indexPath];
}
else {
[super collectionView:collectionView performAction:action forItemAtIndexPath:indexPath withSender:sender];
}
}
- (void)updateGroup {
[self.navController hideDropDown:self];
[self performSegueWithIdentifier:kUpdateGroupSegueIdentifier sender:self];
}
- (void)leaveGroup {
[self.navController hideDropDown:self];
TSGroupThread* gThread = (TSGroupThread*)_thread;
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:gThread messageBody:@"" attachments:[[NSMutableArray alloc] init]];
message.groupMetaMessage = TSGroupMessageQuit;
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
[[TSMessagesManager sharedManager] sendMessage:message inThread:gThread success:nil failure:nil];
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *newGroupMemberIds = [NSMutableArray arrayWithArray:gThread.groupModel.groupMemberIds];
[newGroupMemberIds removeObject:[SignalKeyingStorage.localNumber toE164]];
gThread.groupModel.groupMemberIds = newGroupMemberIds;
[gThread saveWithTransaction:transaction];
}];
[self hideInputIfNeeded];
}
- (void) updateGroupModelTo:(TSGroupModel*)newGroupModel {
__block TSGroupThread *groupThread;
__block TSOutgoingMessage *message;
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
groupThread = [TSGroupThread getOrCreateThreadWithGroupModel:newGroupModel transaction:transaction];
groupThread.groupModel = newGroupModel;
[groupThread saveWithTransaction:transaction];
message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:groupThread messageBody:@"" attachments:[[NSMutableArray alloc] init]];
message.groupMetaMessage = TSGroupMessageUpdate;
}];
if(newGroupModel.groupImage!=nil) {
[[TSMessagesManager sharedManager] sendAttachment:UIImagePNGRepresentation(newGroupModel.groupImage) contentType:@"image/png" inMessage:message thread:groupThread];
}
else {
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
[[TSMessagesManager sharedManager] sendMessage:message inThread:groupThread success:nil failure:nil];
}
self.thread = groupThread;
}
2015-03-19 01:59:44 +01:00
- (IBAction)unwindGroupUpdated:(UIStoryboardSegue *)segue {
NewGroupViewController *ngc = [segue sourceViewController];
TSGroupModel* newGroupModel = [ngc groupModel];
NSMutableSet* groupMemberIds = [NSMutableSet setWithArray:newGroupModel.groupMemberIds];
[groupMemberIds addObject:[SignalKeyingStorage.localNumber toE164]];
newGroupModel.groupMemberIds = [NSMutableArray arrayWithArray:[groupMemberIds allObjects]];
[self updateGroupModelTo:newGroupModel];
[self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]];
[self.collectionView reloadData];
}
- (void)popKeyBoard {
[self.inputToolbar.contentView.textView becomeFirstResponder];
}
- (void)dismissKeyBoard {
[self.inputToolbar.contentView.textView resignFirstResponder];
}
#pragma mark Drafts
2015-03-19 01:59:44 +01:00
- (void)loadDraftInCompose {
__block NSString *placeholder;
[self.editingDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) {
placeholder = [_thread currentDraftWithTransaction:transaction];
} completionBlock:^{
dispatch_async(dispatch_get_main_queue(), ^{
[self.inputToolbar.contentView.textView setText:placeholder];
[self textViewDidChange:self.inputToolbar.contentView.textView];
});
}];
}
2015-03-19 01:59:44 +01:00
- (void)saveDraft {
if (self.inputToolbar.hidden == NO) {
__block TSThread *thread = _thread;
__block NSString *currentDraft = self.inputToolbar.contentView.textView.text;
[self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[thread setDraft:currentDraft transaction:transaction];
}];
}
}
#pragma mark Unread Badge
- (void)setUnreadCount:(NSUInteger)unreadCount {
if (_unreadCount != unreadCount) {
_unreadCount = unreadCount;
if (_unreadCount > 0) {
if (_unreadContainer == nil) {
static UIImage *backgroundImage = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(17.0f, 17.0f), false, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(0.0f, 0.0f, 17.0f, 17.0f));
backgroundImage = [UIGraphicsGetImageFromCurrentImageContext() stretchableImageWithLeftCapWidth:8 topCapHeight:8];
UIGraphicsEndImageContext();
});
_unreadContainer = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 10.0f, 10.0f)];
_unreadContainer.userInteractionEnabled = NO;
_unreadContainer.layer.zPosition = 2000;
[self.navigationController.navigationBar addSubview:_unreadContainer];
_unreadBackground = [[UIImageView alloc] initWithImage:backgroundImage];
[_unreadContainer addSubview:_unreadBackground];
_unreadLabel = [[UILabel alloc] init];
_unreadLabel.backgroundColor = [UIColor clearColor];
_unreadLabel.textColor = [UIColor whiteColor];
_unreadLabel.font = [UIFont systemFontOfSize:12];
[_unreadContainer addSubview:_unreadLabel];
}
_unreadContainer.hidden = false;
_unreadLabel.text = [NSString stringWithFormat:@"%lu", (unsigned long)unreadCount];
[_unreadLabel sizeToFit];
CGPoint offset = CGPointMake(17.0f, 2.0f);
_unreadBackground.frame = CGRectMake(offset.x, offset.y,
MAX(_unreadLabel.frame.size.width + 8.0f, 17.0f), 17.0f);
_unreadLabel.frame = CGRectMake(offset.x + floor((2.0f*(_unreadBackground.frame.size.width - _unreadLabel.frame.size.width)/ 2.0f)/2.0f), offset.y + 1.0f,
_unreadLabel.frame.size.width, _unreadLabel.frame.size.height);
} else if (_unreadContainer != nil) {
_unreadContainer.hidden = true;
}
}
}
2015-03-19 01:59:44 +01:00
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
2014-10-29 21:58:58 +01:00
@end