Support for drafts. Unsent messages are saved in case you want to send them later on and were interrupted while redacting them.
pull/1/head
Frederic Jacobs 11 years ago
parent daac2c0db3
commit ee62cbdf23

@ -11,28 +11,6 @@
@class TSInteraction; @class TSInteraction;
typedef NS_ENUM(NSInteger, TSLastActionType) {
TSLastActionNone,
TSLastActionCallIncoming,
TSLastActionCallIncomingMissed,
TSLastActionCallOutgoing,
TSLastActionCallOutgoingMissed,
TSLastActionCallOutgoingFailed,
TSLastActionMessageAttemptingOut,
TSLastActionMessageUnsent,
TSLastActionMessageSent,
TSLastActionMessageDelivered,
TSLastActionMessageIncomingRead,
TSLastActionMessageIncomingUnread,
TSLastActionInfoMessage,
TSLastActionErrorMessage
};
/** /**
* TSThread is the superclass of TSContactThread and TSGroupThread * TSThread is the superclass of TSContactThread and TSGroupThread
*/ */
@ -40,45 +18,112 @@ typedef NS_ENUM(NSInteger, TSLastActionType) {
@interface TSThread : TSYapDatabaseObject @interface TSThread : TSYapDatabaseObject
/** /**
* Returns whether the object is a group thread or not * Whether the object is a group thread or not.
* *
* @return Is a group * @return YES if is a group thread, NO otherwise.
*/ */
- (BOOL)isGroupThread; - (BOOL)isGroupThread;
/** /**
* Returns the name of the thread. * Returns the name of the thread.
* *
* @return name of the thread * @return The name of the thread.
*/ */
- (NSString *)name;
- (NSString*)name;
/** /**
* Returns the image representing the thread. Nil if not available. * Returns the image representing the thread. Nil if not available.
* *
* @return UIImage of the thread, or nil. * @return UIImage of the thread, or nil.
*/ */
- (UIImage *)image;
#pragma mark Read Status
- (UIImage*)image; /**
* Returns whether or not the thread has unread messages.
*
* @return YES if it has unread TSIncomingMessages, NO otherwise.
*/
- (BOOL)hasUnreadMessages;
- (NSDate*)lastMessageDate; - (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (NSString*)lastMessageLabel;
- (NSDate*)archivalDate;
- (void)updateWithLastMessage:(TSInteraction*)lastMessage transaction:(YapDatabaseReadWriteTransaction*)transaction; #pragma mark Last Interactions
- (TSLastActionType)lastAction; /**
* Returns the latest date of a message in the thread or the thread creation date if there are no messages in that
*thread.
*
* @return The date of the last message or thread creation date.
*/
- (NSDate *)lastMessageDate;
- (BOOL)hasUnreadMessages; /**
* Returns the string that will be displayed typically in a conversations view as a preview of the last message
*received in this thread.
*
* @return Thread preview string.
*/
- (NSString *)lastMessageLabel;
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction; /**
* Updates the thread's caches of the latest interaction.
*
* @param lastMessage Latest Interaction to take into consideration.
* @param transaction Database transaction.
*/
- (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction; #pragma mark Archival
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction referenceDate:(NSDate*)date;
- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction; /**
* Returns the last date at which a string was archived or nil if the thread was never archived or brought back to the
*inbox.
*
* @return Last archival date.
*/
- (NSDate *)archivalDate;
/**
* Archives a thread with the current date.
*
* @param transaction Database transaction.
*/
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
/**
* Archives a thread with the reference date. This is currently only used for migrating older data that has already been archived.
*
* @param transaction Database transaction.
* @param date Date at which the thread was archived.
*/
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction referenceDate:(NSDate *)date;
/**
* Unarchives a thread that was archived previously.
*
* @param transaction Database transaction.
*/
- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark Drafts
/**
* Returns the last known draft for that thread. Always returns a string. Empty string if nil.
*
* @param transaction Database transaction.
*
* @return Last known draft for that thread.
*/
- (NSString *)currentDraftWithTransaction:(YapDatabaseReadTransaction *)transaction;
/**
* Sets the draft of a thread. Typically called when leaving a conversation view.
*
* @param draftString Draft string to be saved.
* @param transaction Database transaction.
*/
- (void)setDraft:(NSString *)draftString transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end @end

@ -21,19 +21,21 @@
@interface TSThread () @interface TSThread ()
@property (nonatomic, retain) NSDate *creationDate; @property (nonatomic, retain) NSDate *creationDate;
@property (nonatomic, copy) NSDate *archivalDate; @property (nonatomic, copy ) NSDate *archivalDate;
@property (nonatomic, retain) NSDate *lastMessageDate; @property (nonatomic, retain) NSDate *lastMessageDate;
@property (nonatomic, copy ) NSString *latestMessageId; @property (nonatomic, copy ) NSString *latestMessageId;
@property (nonatomic, copy ) NSString *messageDraft;
@end @end
@implementation TSThread @implementation TSThread
+ (NSString *)collection{ + (NSString *)collection
{
return @"TSThread"; return @"TSThread";
} }
- (instancetype)initWithUniqueId:(NSString *)uniqueId{ - (instancetype)initWithUniqueId:(NSString *)uniqueId
{
self = [super initWithUniqueId:uniqueId]; self = [super initWithUniqueId:uniqueId];
if (self) { if (self) {
@ -41,146 +43,136 @@
_latestMessageId = nil; _latestMessageId = nil;
_lastMessageDate = nil; _lastMessageDate = nil;
_creationDate = [NSDate date]; _creationDate = [NSDate date];
_messageDraft = nil;
} }
return self; return self;
} }
- (BOOL)isGroupThread{ #pragma mark To be subclassed.
- (BOOL)isGroupThread
{
NSAssert(false, @"An abstract method on TSThread was called."); NSAssert(false, @"An abstract method on TSThread was called.");
return FALSE; return FALSE;
} }
- (NSDate *)lastMessageDate{ - (NSString *)name
if (_lastMessageDate) { {
return _lastMessageDate; NSAssert(FALSE, @"Should be implemented in subclasses");
} else { return nil;
return _creationDate;
}
} }
- (UIImage*)image{ - (UIImage *)image
{
return nil; return nil;
} }
- (NSDate *)archivalDate{ #pragma mark Read Status
return _archivalDate;
}
- (NSString*)lastMessageLabel{ - (BOOL)hasUnreadMessages
{
__block TSInteraction *interaction; __block TSInteraction *interaction;
__block BOOL hasUnread = NO;
[[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction]; interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction];
if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
hasUnread = ![(TSIncomingMessage *)interaction wasRead];
}
}]; }];
return interaction.description;
return hasUnread;
} }
- (TSLastActionType)lastAction - (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{ {
__block TSInteraction *interaction; YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSUnreadDatabaseViewExtensionName];
[[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { NSMutableArray *array = [NSMutableArray array];
interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction]; [viewTransaction enumerateRowsInGroup:self.uniqueId
}]; usingBlock:^(NSString *collection, NSString *key, id object, id metadata,
NSUInteger index, BOOL *stop) {
[array addObject:object];
}];
for (TSIncomingMessage *message in array) {
message.read = YES;
[message saveWithTransaction:transaction];
}
}
return [self lastActionForInteraction:interaction]; #pragma mark Last Interactions
}
- (NSDate *)lastMessageDate
- (TSLastActionType)lastActionForInteraction:(TSInteraction*)interaction {
{ if (_lastMessageDate) {
if ([interaction isKindOfClass:[TSCall class]]) return _lastMessageDate;
{
TSCall * callInteraction = (TSCall*)interaction;
switch (callInteraction.callType) {
case RPRecentCallTypeMissed:
return TSLastActionCallIncomingMissed;
case RPRecentCallTypeIncoming:
return TSLastActionCallIncoming;
case RPRecentCallTypeOutgoing:
return TSLastActionCallOutgoing;
default:
return TSLastActionNone;
}
} else if ([interaction isKindOfClass:[TSOutgoingMessage class]]) {
TSOutgoingMessage * outgoingMessageInteraction = (TSOutgoingMessage*)interaction;
switch (outgoingMessageInteraction.messageState) {
case TSOutgoingMessageStateAttemptingOut:
return TSLastActionNone;
case TSOutgoingMessageStateUnsent:
return TSLastActionMessageUnsent;
case TSOutgoingMessageStateSent:
return TSLastActionMessageSent;
case TSOutgoingMessageStateDelivered:
return TSLastActionMessageDelivered;
default:
return TSLastActionNone;
}
} else if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
return self.hasUnreadMessages ? TSLastActionMessageIncomingUnread : TSLastActionMessageIncomingRead ;
} else if ([interaction isKindOfClass:[TSErrorMessage class]]) {
return TSLastActionErrorMessage;
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
return TSLastActionInfoMessage;
} else { } else {
return TSLastActionNone; return _creationDate;
} }
} }
- (BOOL)hasUnreadMessages{ - (NSString *)lastMessageLabel
__block TSInteraction * interaction; {
__block BOOL hasUnread = NO; __block TSInteraction *interaction;
[[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction]; interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction];
if ([interaction isKindOfClass:[TSIncomingMessage class]]){
hasUnread = ![(TSIncomingMessage*)interaction wasRead];
}
}]; }];
return interaction.description;
return hasUnread;
} }
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction { - (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(YapDatabaseReadWriteTransaction *)transaction
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSUnreadDatabaseViewExtensionName]; {
NSMutableArray *array = [NSMutableArray array]; if (!_lastMessageDate || [lastMessage.date timeIntervalSinceDate:self.lastMessageDate] > 0) {
[viewTransaction enumerateRowsInGroup:self.uniqueId usingBlock:^(NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { _latestMessageId = lastMessage.uniqueId;
[array addObject:object]; _lastMessageDate = lastMessage.date;
}];
for (TSIncomingMessage *message in array) { [self saveWithTransaction:transaction];
message.read = YES;
[message saveWithTransaction:transaction];
} }
} }
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { #pragma mark Archival
- (NSDate *)archivalDate
{
return _archivalDate;
}
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[self archiveThreadWithTransaction:transaction referenceDate:[NSDate date]]; [self archiveThreadWithTransaction:transaction referenceDate:[NSDate date]];
} }
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction referenceDate:(NSDate*)date { - (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction referenceDate:(NSDate *)date
{
[self markAllAsReadWithTransaction:transaction]; [self markAllAsReadWithTransaction:transaction];
_archivalDate = date; _archivalDate = date;
[self saveWithTransaction:transaction]; [self saveWithTransaction:transaction];
} }
- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction { - (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
_archivalDate = nil; _archivalDate = nil;
[self saveWithTransaction:transaction]; [self saveWithTransaction:transaction];
} }
- (void)updateWithLastMessage:(TSInteraction*)lastMessage transaction:(YapDatabaseReadWriteTransaction*)transaction { #pragma mark Drafts
if (!_lastMessageDate || [lastMessage.date timeIntervalSinceDate:self.lastMessageDate] > 0) {
_latestMessageId = lastMessage.uniqueId; - (NSString *)currentDraftWithTransaction:(YapDatabaseReadTransaction *)transaction
_lastMessageDate = lastMessage.date; {
[self saveWithTransaction:transaction]; TSThread *thread = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction];
if (thread.messageDraft) {
return thread.messageDraft;
} else {
return @"";
} }
} }
- (NSString *)name{ - (void)setDraft:(NSString *)draftString transaction:(YapDatabaseReadWriteTransaction *)transaction
NSAssert(FALSE, @"Should be implemented in subclasses"); {
return nil; TSThread *thread = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction];
thread.messageDraft = draftString;
[thread saveWithTransaction:transaction];
} }
@end @end

@ -11,14 +11,27 @@
#import "UIColor+OWS.h" #import "UIColor+OWS.h"
@implementation UIButton (OWS) @implementation UIButton (OWS)
+ (UIButton*) ows_blueButtonWithTitle:(NSString*)title { + (UIButton *)ows_blueButtonWithTitle:(NSString *)title
NSDictionary* buttonTextAttributes = @{NSFontAttributeName:[UIFont ows_regularFontWithSize:15.0f], {
NSForegroundColorAttributeName:[UIColor ows_materialBlueColor]}; NSDictionary *buttonTextAttributes = @{
UIButton* button = [[UIButton alloc] init]; NSFontAttributeName : [UIFont ows_regularFontWithSize:15.0f],
NSForegroundColorAttributeName : [UIColor ows_materialBlueColor]
};
UIButton *button = [[UIButton alloc] init];
NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:title]; NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:title];
[attributedTitle setAttributes:buttonTextAttributes range:NSMakeRange(0, [attributedTitle length])]; [attributedTitle setAttributes:buttonTextAttributes range:NSMakeRange(0, [attributedTitle length])];
[button setAttributedTitle:attributedTitle forState:UIControlStateNormal]; [button setAttributedTitle:attributedTitle forState:UIControlStateNormal];
NSDictionary *disabledAttributes = @{
NSFontAttributeName : [UIFont ows_regularFontWithSize:15.0f],
NSForegroundColorAttributeName : [UIColor ows_darkGrayColor]
};
NSMutableAttributedString *attributedTitleDisabled = [[NSMutableAttributedString alloc] initWithString:title];
[attributedTitleDisabled setAttributes:disabledAttributes range:NSMakeRange(0, [attributedTitle length])];
[button setAttributedTitle:attributedTitleDisabled forState:UIControlStateDisabled];
[button.titleLabel setTextAlignment:NSTextAlignmentCenter]; [button.titleLabel setTextAlignment:NSTextAlignmentCenter];
return button; return button;
} }

@ -141,13 +141,15 @@ typedef enum : NSUInteger {
} }
-(void) hideInputIfNeeded { - (void)hideInputIfNeeded {
if([_thread isKindOfClass:[TSGroupThread class]] && ![((TSGroupThread*)_thread).groupModel.groupMemberIds containsObject:[SignalKeyingStorage.localNumber toE164]]) { 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 inputToolbar].hidden= YES; // user has requested they leave the group. further sends disallowed
self.navigationItem.rightBarButtonItem = nil; // further group action disallowed self.navigationItem.rightBarButtonItem = nil; // further group action disallowed
} }
else if(![self isTextSecureReachable] ){ else if(![self isTextSecureReachable] ){
[self inputToolbar].hidden= YES; // only RedPhone [self inputToolbar].hidden= YES; // only RedPhone
} else {
[self loadDraftInCompose];
} }
} }
@ -162,6 +164,7 @@ typedef enum : NSUInteger {
_toggleContactPhoneDisplay.numberOfTapsRequired = 1; _toggleContactPhoneDisplay.numberOfTapsRequired = 1;
_messageButton = [UIButton ows_blueButtonWithTitle:NSLocalizedString(@"SEND_BUTTON_TITLE", @"")]; _messageButton = [UIButton ows_blueButtonWithTitle:NSLocalizedString(@"SEND_BUTTON_TITLE", @"")];
_messageButton.enabled = FALSE;
_attachButton = [[UIButton alloc] init]; _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 setFrame:CGRectMake(0, 0, JSQ_TOOLBAR_ICON_WIDTH+JSQ_IMAGE_INSET*2, JSQ_TOOLBAR_ICON_HEIGHT+JSQ_IMAGE_INSET*2)];
@ -198,9 +201,9 @@ typedef enum : NSUInteger {
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 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 { - (void)initializeTextView {
[self.inputToolbar.contentView.textView setFont:[UIFont ows_regularFontWithSize:17.f]]; [self.inputToolbar.contentView.textView setFont:[UIFont ows_regularFontWithSize:17.f]];
self.inputToolbar.contentView.leftBarButtonItem = _attachButton; self.inputToolbar.contentView.leftBarButtonItem = _attachButton;
self.inputToolbar.contentView.rightBarButtonItem = _messageButton; self.inputToolbar.contentView.rightBarButtonItem = _messageButton;
} }
@ -263,6 +266,7 @@ typedef enum : NSUInteger {
[self cancelReadTimer]; [self cancelReadTimer];
[self removeTitleLabelGestureRecognizer]; [self removeTitleLabelGestureRecognizer];
[self saveDraft];
} }
- (void)viewDidDisappear:(BOOL)animated{ - (void)viewDidDisappear:(BOOL)animated{
@ -527,7 +531,6 @@ typedef enum : NSUInteger {
- (void)textViewDidChange:(UITextView *)textView { - (void)textViewDidChange:(UITextView *)textView {
if([textView.text length]>0) { if([textView.text length]>0) {
self.inputToolbar.contentView.rightBarButtonItem = _messageButton;
self.inputToolbar.contentView.rightBarButtonItem.enabled = YES; self.inputToolbar.contentView.rightBarButtonItem.enabled = YES;
} }
else { else {
@ -1186,7 +1189,6 @@ typedef enum : NSUInteger {
[self dismissViewControllerAnimated:YES completion:^{ [self dismissViewControllerAnimated:YES completion:^{
[[TSMessagesManager sharedManager] sendAttachment:attachmentData contentType:attachmentType inMessage:message thread:self.thread]; [[TSMessagesManager sharedManager] sendAttachment:attachmentData contentType:attachmentType inMessage:message thread:self.thread];
[self finishSendingMessage];
}]; }];
} }
@ -1310,7 +1312,6 @@ typedef enum : NSUInteger {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
TSGroupThread* gThread = (TSGroupThread*)self.thread; TSGroupThread* gThread = (TSGroupThread*)self.thread;
self.thread = [TSGroupThread threadWithGroupModel:gThread.groupModel transaction:transaction]; self.thread = [TSGroupThread threadWithGroupModel:gThread.groupModel transaction:transaction];
[self initializeToolbars];
}]; }];
} }
@ -1560,7 +1561,6 @@ typedef enum : NSUInteger {
[self performSegueWithIdentifier:kUpdateGroupSegueIdentifier sender:self]; [self performSegueWithIdentifier:kUpdateGroupSegueIdentifier sender:self];
} }
- (void)leaveGroup { - (void)leaveGroup {
[self.navController hideDropDown:self]; [self.navController hideDropDown:self];
@ -1615,6 +1615,30 @@ typedef enum : NSUInteger {
[self.inputToolbar.contentView.textView resignFirstResponder]; [self.inputToolbar.contentView.textView resignFirstResponder];
} }
#pragma mark Drafts
- (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];
});
}];
}
- (void)saveDraft
{
if (self.inputToolbar.hidden == NO) {
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[_thread setDraft:self.inputToolbar.contentView.textView.text transaction:transaction];
}];
}
}
- (void)dealloc{ - (void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self];
} }

Loading…
Cancel
Save