From ee62cbdf237cc1665df032dbcad5b5805174ae9e Mon Sep 17 00:00:00 2001 From: Frederic Jacobs Date: Sun, 1 Mar 2015 00:04:39 +0100 Subject: [PATCH] Fixes #404 Support for drafts. Unsent messages are saved in case you want to send them later on and were interrupted while redacting them. --- Signal/src/textsecure/Contacts/TSThread.h | 123 ++++++++---- Signal/src/textsecure/Contacts/TSThread.m | 190 +++++++++--------- Signal/src/util/UIButton+OWS.m | 23 ++- .../view controllers/MessagesViewController.m | 38 +++- 4 files changed, 224 insertions(+), 150 deletions(-) diff --git a/Signal/src/textsecure/Contacts/TSThread.h b/Signal/src/textsecure/Contacts/TSThread.h index 3898ce243..312d4b181 100644 --- a/Signal/src/textsecure/Contacts/TSThread.h +++ b/Signal/src/textsecure/Contacts/TSThread.h @@ -11,28 +11,6 @@ @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 */ @@ -40,45 +18,112 @@ typedef NS_ENUM(NSInteger, TSLastActionType) { @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; /** * 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. * * @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; -- (NSString*)lastMessageLabel; -- (NSDate*)archivalDate; +- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; -- (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; + +/** + * 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; + +#pragma mark Archival + +/** + * 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; -- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction; +/** + * Archives a thread with the current date. + * + * @param transaction Database transaction. + */ +- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; -- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction; -- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction referenceDate:(NSDate*)date; +/** + * 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; -- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction; +/** + * 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 diff --git a/Signal/src/textsecure/Contacts/TSThread.m b/Signal/src/textsecure/Contacts/TSThread.m index 60b2e51de..b6edfbf96 100644 --- a/Signal/src/textsecure/Contacts/TSThread.m +++ b/Signal/src/textsecure/Contacts/TSThread.m @@ -21,166 +21,158 @@ @interface TSThread () @property (nonatomic, retain) NSDate *creationDate; -@property (nonatomic, copy) NSDate *archivalDate; +@property (nonatomic, copy ) NSDate *archivalDate; @property (nonatomic, retain) NSDate *lastMessageDate; @property (nonatomic, copy ) NSString *latestMessageId; - +@property (nonatomic, copy ) NSString *messageDraft; @end @implementation TSThread -+ (NSString *)collection{ ++ (NSString *)collection +{ return @"TSThread"; } -- (instancetype)initWithUniqueId:(NSString *)uniqueId{ +- (instancetype)initWithUniqueId:(NSString *)uniqueId +{ self = [super initWithUniqueId:uniqueId]; - + if (self) { _archivalDate = nil; _latestMessageId = nil; _lastMessageDate = nil; _creationDate = [NSDate date]; + _messageDraft = nil; } - + return self; } -- (BOOL)isGroupThread{ +#pragma mark To be subclassed. + +- (BOOL)isGroupThread +{ NSAssert(false, @"An abstract method on TSThread was called."); return FALSE; } -- (NSDate *)lastMessageDate{ - if (_lastMessageDate) { - return _lastMessageDate; - } else { - return _creationDate; - } +- (NSString *)name +{ + NSAssert(FALSE, @"Should be implemented in subclasses"); + return nil; } -- (UIImage*)image{ +- (UIImage *)image +{ return nil; } -- (NSDate *)archivalDate{ - return _archivalDate; -} +#pragma mark Read Status -- (NSString*)lastMessageLabel{ +- (BOOL)hasUnreadMessages +{ __block TSInteraction *interaction; + __block BOOL hasUnread = NO; [[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; - [[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction]; - }]; - - return [self lastActionForInteraction:interaction]; -} - -- (TSLastActionType)lastActionForInteraction:(TSInteraction*)interaction -{ - if ([interaction isKindOfClass:[TSCall class]]) - { - 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; + YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSUnreadDatabaseViewExtensionName]; + NSMutableArray *array = [NSMutableArray array]; + [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]; + } +} + +#pragma mark Last Interactions + +- (NSDate *)lastMessageDate +{ + if (_lastMessageDate) { + return _lastMessageDate; } else { - return TSLastActionNone; + return _creationDate; } } -- (BOOL)hasUnreadMessages{ - __block TSInteraction * interaction; - __block BOOL hasUnread = NO; +- (NSString *)lastMessageLabel +{ + __block TSInteraction *interaction; [[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction]; - if ([interaction isKindOfClass:[TSIncomingMessage class]]){ - hasUnread = ![(TSIncomingMessage*)interaction wasRead]; - } + interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction]; }]; - - return hasUnread; + return interaction.description; } -- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction { - YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSUnreadDatabaseViewExtensionName]; - NSMutableArray *array = [NSMutableArray array]; - [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]; +- (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!_lastMessageDate || [lastMessage.date timeIntervalSinceDate:self.lastMessageDate] > 0) { + _latestMessageId = lastMessage.uniqueId; + _lastMessageDate = lastMessage.date; + + [self saveWithTransaction:transaction]; } } -- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { +#pragma mark Archival + +- (NSDate *)archivalDate +{ + return _archivalDate; +} + +- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ [self archiveThreadWithTransaction:transaction referenceDate:[NSDate date]]; } -- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction referenceDate:(NSDate*)date { +- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction referenceDate:(NSDate *)date +{ [self markAllAsReadWithTransaction:transaction]; _archivalDate = date; - + [self saveWithTransaction:transaction]; } -- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction { +- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ _archivalDate = nil; [self saveWithTransaction:transaction]; } -- (void)updateWithLastMessage:(TSInteraction*)lastMessage transaction:(YapDatabaseReadWriteTransaction*)transaction { - if (!_lastMessageDate || [lastMessage.date timeIntervalSinceDate:self.lastMessageDate] > 0) { - _latestMessageId = lastMessage.uniqueId; - _lastMessageDate = lastMessage.date; - [self saveWithTransaction:transaction]; +#pragma mark Drafts + +- (NSString *)currentDraftWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + TSThread *thread = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction]; + if (thread.messageDraft) { + return thread.messageDraft; + } else { + return @""; } } -- (NSString *)name{ - NSAssert(FALSE, @"Should be implemented in subclasses"); - return nil; +- (void)setDraft:(NSString *)draftString transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + TSThread *thread = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction]; + thread.messageDraft = draftString; + [thread saveWithTransaction:transaction]; } @end diff --git a/Signal/src/util/UIButton+OWS.m b/Signal/src/util/UIButton+OWS.m index bdcc19ffb..823016fa9 100644 --- a/Signal/src/util/UIButton+OWS.m +++ b/Signal/src/util/UIButton+OWS.m @@ -11,15 +11,28 @@ #import "UIColor+OWS.h" @implementation UIButton (OWS) -+ (UIButton*) ows_blueButtonWithTitle:(NSString*)title { - NSDictionary* buttonTextAttributes = @{NSFontAttributeName:[UIFont ows_regularFontWithSize:15.0f], - NSForegroundColorAttributeName:[UIColor ows_materialBlueColor]}; - UIButton* button = [[UIButton alloc] init]; ++ (UIButton *)ows_blueButtonWithTitle:(NSString *)title +{ + NSDictionary *buttonTextAttributes = @{ + NSFontAttributeName : [UIFont ows_regularFontWithSize:15.0f], + NSForegroundColorAttributeName : [UIColor ows_materialBlueColor] + }; + UIButton *button = [[UIButton alloc] init]; NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:title]; [attributedTitle setAttributes:buttonTextAttributes range:NSMakeRange(0, [attributedTitle length])]; [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]; - return button; + + return button; } @end diff --git a/Signal/src/view controllers/MessagesViewController.m b/Signal/src/view controllers/MessagesViewController.m index 78fd65ef1..23f324685 100644 --- a/Signal/src/view controllers/MessagesViewController.m +++ b/Signal/src/view controllers/MessagesViewController.m @@ -141,13 +141,15 @@ typedef enum : NSUInteger { } --(void) hideInputIfNeeded { +- (void)hideInputIfNeeded { 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 loadDraftInCompose]; } } @@ -162,6 +164,7 @@ typedef enum : NSUInteger { _toggleContactPhoneDisplay.numberOfTapsRequired = 1; _messageButton = [UIButton ows_blueButtonWithTitle:NSLocalizedString(@"SEND_BUTTON_TITLE", @"")]; + _messageButton.enabled = FALSE; _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)]; @@ -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 } --(void) initializeTextView { +- (void)initializeTextView { [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; } @@ -263,6 +266,7 @@ typedef enum : NSUInteger { [self cancelReadTimer]; [self removeTitleLabelGestureRecognizer]; + [self saveDraft]; } - (void)viewDidDisappear:(BOOL)animated{ @@ -527,7 +531,6 @@ typedef enum : NSUInteger { - (void)textViewDidChange:(UITextView *)textView { if([textView.text length]>0) { - self.inputToolbar.contentView.rightBarButtonItem = _messageButton; self.inputToolbar.contentView.rightBarButtonItem.enabled = YES; } else { @@ -1186,7 +1189,6 @@ typedef enum : NSUInteger { [self dismissViewControllerAnimated:YES completion:^{ [[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) { TSGroupThread* gThread = (TSGroupThread*)self.thread; self.thread = [TSGroupThread threadWithGroupModel:gThread.groupModel transaction:transaction]; - [self initializeToolbars]; }]; } @@ -1560,7 +1561,6 @@ typedef enum : NSUInteger { [self performSegueWithIdentifier:kUpdateGroupSegueIdentifier sender:self]; } - - (void)leaveGroup { [self.navController hideDropDown:self]; @@ -1615,6 +1615,30 @@ typedef enum : NSUInteger { [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{ [[NSNotificationCenter defaultCenter] removeObserver:self]; }