Support for drafts. Unsent messages are saved in case you want to send them later on and were interrupted while redacting them.
This commit is contained in:
Frederic Jacobs 2015-03-01 00:04:39 +01:00
parent daac2c0db3
commit ee62cbdf23
4 changed files with 244 additions and 170 deletions

View File

@ -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;
- (NSDate*)lastMessageDate;
- (NSString*)lastMessageLabel;
- (NSDate*)archivalDate;
- (void)updateWithLastMessage:(TSInteraction*)lastMessage transaction:(YapDatabaseReadWriteTransaction*)transaction;
- (TSLastActionType)lastAction;
/**
* Returns whether or not the thread has unread messages.
*
* @return YES if it has unread TSIncomingMessages, NO otherwise.
*/
- (BOOL)hasUnreadMessages;
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction;
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction;
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction referenceDate:(NSDate*)date;
#pragma mark Last Interactions
- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction;
/**
* 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;
/**
* 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;
/**
* 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

View File

@ -21,37 +21,89 @@
@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{
- (NSString *)name
{
NSAssert(FALSE, @"Should be implemented in subclasses");
return nil;
}
- (UIImage *)image
{
return nil;
}
#pragma mark Read Status
- (BOOL)hasUnreadMessages
{
__block TSInteraction *interaction;
__block BOOL hasUnread = NO;
[[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction];
if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
hasUnread = ![(TSIncomingMessage *)interaction wasRead];
}
}];
return hasUnread;
}
- (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];
}
}
#pragma mark Last Interactions
- (NSDate *)lastMessageDate
{
if (_lastMessageDate) {
return _lastMessageDate;
} else {
@ -59,128 +111,68 @@
}
}
- (UIImage*)image{
return nil;
}
- (NSDate *)archivalDate{
return _archivalDate;
}
- (NSString*)lastMessageLabel{
- (NSString *)lastMessageLabel
{
__block TSInteraction *interaction;
[[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction];
interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction];
}];
return interaction.description;
}
- (TSLastActionType)lastAction
- (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(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;
} else {
return TSLastActionNone;
}
}
- (BOOL)hasUnreadMessages{
__block TSInteraction * interaction;
__block BOOL hasUnread = NO;
[[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
interaction = [TSInteraction fetchObjectWithUniqueID:self.latestMessageId transaction:transaction];
if ([interaction isKindOfClass:[TSIncomingMessage class]]){
hasUnread = ![(TSIncomingMessage*)interaction wasRead];
}
}];
return hasUnread;
}
- (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)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction {
[self archiveThreadWithTransaction:transaction referenceDate:[NSDate date]];
}
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction*)transaction referenceDate:(NSDate*)date {
[self markAllAsReadWithTransaction:transaction];
_archivalDate = date;
[self saveWithTransaction: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];
}
}
- (NSString *)name{
NSAssert(FALSE, @"Should be implemented in subclasses");
return nil;
#pragma mark Archival
- (NSDate *)archivalDate
{
return _archivalDate;
}
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[self archiveThreadWithTransaction:transaction referenceDate:[NSDate date]];
}
- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction referenceDate:(NSDate *)date
{
[self markAllAsReadWithTransaction:transaction];
_archivalDate = date;
[self saveWithTransaction:transaction];
}
- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
_archivalDate = nil;
[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 @"";
}
}
- (void)setDraft:(NSString *)draftString transaction:(YapDatabaseReadWriteTransaction *)transaction
{
TSThread *thread = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction];
thread.messageDraft = draftString;
[thread saveWithTransaction:transaction];
}
@end

View File

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

View File

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