Tweak system messages; incomplete vs. missed calls.

pull/1/head
Matthew Chen 7 years ago
parent 8b3bdb88f3
commit 158aa3abc4

@ -1,23 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "system_message_group@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "system_message_group@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "system_message_group@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

@ -1,23 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "system_message_info@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "system_message_info@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "system_message_info@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

@ -40,6 +40,7 @@
#import <SignalServiceKit/OWSDisappearingMessagesJob.h>
#import <SignalServiceKit/OWSFailedAttachmentDownloadsJob.h>
#import <SignalServiceKit/OWSFailedMessagesJob.h>
#import <SignalServiceKit/OWSIncompleteCallsJob.h>
#import <SignalServiceKit/OWSMessageManager.h>
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/OWSOrphanedDataCleaner.h>
@ -596,6 +597,9 @@ static NSTimeInterval launchStartedAt;
// Mark all "attempting out" messages as "unsent", i.e. any messages that were not successfully
// sent before the app exited should be marked as failures.
[[[OWSFailedMessagesJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] run];
// Mark all "incomplete" calls as missed, e.g. any incoming or outgoing calls that were not
// connected, failed or hung up before the app existed should be marked as missed.
[[[OWSIncompleteCallsJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] run];
[[[OWSFailedAttachmentDownloadsJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]]
run];

@ -11,11 +11,16 @@ NS_ASSUME_NONNULL_BEGIN
@class OWSContactsManager;
@class TSAttachmentPointer;
@class TSAttachmentStream;
@class TSCall;
@class TSErrorMessage;
@class TSInteraction;
@class TSInvalidIdentityKeyErrorMessage;
@class TSInvalidIdentityKeyErrorMessage;
@class TSMessage;
@class TSOutgoingMessage;
@class TSQuotedMessage;
@class YapDatabaseReadTransaction;
@class YapDatabaseReadTransaction;
@protocol ConversationViewCellDelegate <NSObject>
@ -27,8 +32,13 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - System Cell
// TODO: We might want to decompose this method.
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction;
- (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalId;
- (void)tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)errorMessage;
- (void)tappedCorruptedMessage:(TSErrorMessage *)message;
- (void)resendGroupUpdateForErrorMessage:(TSErrorMessage *)message;
- (void)showFingerprintWithRecipientId:(NSString *)recipientId;
- (void)showConversationSettings;
- (void)handleCallTap:(TSCall *)call;
#pragma mark - Offers

@ -17,14 +17,41 @@
NS_ASSUME_NONNULL_BEGIN
typedef void (^SystemMessageActionBlock)(void);
@interface SystemMessageAction : NSObject
@property (nonatomic) NSString *title;
@property (nonatomic) SystemMessageActionBlock block;
@end
#pragma mark -
@implementation SystemMessageAction
+ (SystemMessageAction *)actionWithTitle:(NSString *)title block:(SystemMessageActionBlock)block
{
SystemMessageAction *action = [SystemMessageAction new];
action.title = title;
action.block = block;
return action;
}
@end
#pragma mark -
@interface OWSSystemMessageCell ()
@property (nonatomic, nullable) TSInteraction *interaction;
@property (nonatomic) UIImageView *imageView;
@property (nonatomic) UIImageView *iconView;
@property (nonatomic) UILabel *titleLabel;
@property (nonatomic) UIStackView *stackView;
@property (nonatomic) UIButton *button;
@property (nonatomic) UIStackView *vStackView;
@property (nonatomic) NSArray<NSLayoutConstraint *> *layoutConstraints;
@property (nonatomic, nullable) SystemMessageAction *action;
@end
@ -44,38 +71,71 @@ NS_ASSUME_NONNULL_BEGIN
- (void)commontInit
{
OWSAssert(!self.imageView);
OWSAssert(!self.iconView);
self.layoutMargins = UIEdgeInsetsZero;
self.contentView.layoutMargins = UIEdgeInsetsZero;
self.imageView = [UIImageView new];
[self.imageView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
[self.imageView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
[self.imageView setContentHuggingHigh];
self.iconView = [UIImageView new];
[self.iconView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
[self.iconView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
[self.iconView setContentHuggingHigh];
self.titleLabel = [UILabel new];
self.titleLabel.numberOfLines = 0;
self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
self.titleLabel.textAlignment = NSTextAlignmentCenter;
self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.imageView,
UIStackView *contentStackView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.iconView,
self.titleLabel,
]];
self.stackView.axis = UILayoutConstraintAxisHorizontal;
self.stackView.spacing = self.hSpacing;
self.stackView.alignment = UIStackViewAlignmentCenter;
[self.contentView addSubview:self.stackView];
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
[self addGestureRecognizer:tap];
contentStackView.axis = UILayoutConstraintAxisVertical;
contentStackView.spacing = self.iconVSpacing;
contentStackView.alignment = UIStackViewAlignmentCenter;
self.button = [UIButton buttonWithType:UIButtonTypeCustom];
[self.button setTitleColor:[UIColor ows_darkSkyBlueColor] forState:UIControlStateNormal];
self.button.titleLabel.textAlignment = NSTextAlignmentCenter;
[self.button setBackgroundColor:[UIColor ows_light02Color]];
self.button.layer.cornerRadius = 4.f;
[self.button addTarget:self action:@selector(buttonWasPressed:) forControlEvents:UIControlEventTouchUpInside];
[self.button autoSetDimension:ALDimensionHeight toSize:self.buttonHeight];
self.vStackView = [[UIStackView alloc] initWithArrangedSubviews:@[
contentStackView,
self.button,
]];
self.vStackView.axis = UILayoutConstraintAxisVertical;
self.vStackView.spacing = self.buttonVSpacing;
self.vStackView.alignment = UIStackViewAlignmentCenter;
[self.contentView addSubview:self.vStackView];
UILongPressGestureRecognizer *longPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
[self addGestureRecognizer:longPress];
}
- (CGFloat)buttonVSpacing
{
return 7.f;
}
- (CGFloat)iconVSpacing
{
return 9.f;
}
- (CGFloat)buttonHeight
{
return 40.f;
}
- (CGFloat)buttonHPadding
{
return 20.f;
}
- (void)configureFonts
{
// Update cell to reflect changes in dynamic text.
@ -95,27 +155,43 @@ NS_ASSUME_NONNULL_BEGIN
TSInteraction *interaction = self.viewItem.interaction;
UIImage *icon = [self iconForInteraction:interaction];
self.imageView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
self.imageView.tintColor = [self iconColorForInteraction:interaction];
self.action = [self actionForInteraction:interaction];
UIImage *_Nullable icon = [self iconForInteraction:interaction];
if (icon) {
self.iconView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
self.iconView.hidden = NO;
self.iconView.tintColor = [self iconColorForInteraction:interaction];
} else {
self.iconView.hidden = YES;
}
self.titleLabel.textColor = [self textColor];
[self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction];
CGSize titleSize = [self titleSize];
if (self.action) {
[self.button setTitle:self.action.title forState:UIControlStateNormal];
UIFont *buttonFont = UIFont.ows_dynamicTypeSubheadlineFont.ows_mediumWeight;
self.button.titleLabel.font = buttonFont;
self.button.hidden = NO;
} else {
self.button.hidden = YES;
}
CGSize buttonSize = [self.button sizeThatFits:CGSizeZero];
[NSLayoutConstraint deactivateConstraints:self.layoutConstraints];
self.layoutConstraints = @[
[self.titleLabel autoSetDimension:ALDimensionWidth toSize:titleSize.width],
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.topVMargin],
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.bottomVMargin],
// H-center the stack.
[self.stackView autoHCenterInSuperview],
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading
withInset:self.conversationStyle.fullWidthGutterLeading
relation:NSLayoutRelationGreaterThanOrEqual],
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
withInset:self.conversationStyle.fullWidthGutterTrailing
relation:NSLayoutRelationGreaterThanOrEqual],
[self.button autoSetDimension:ALDimensionWidth toSize:buttonSize.width + self.buttonHPadding * 2.f],
[self.vStackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.topVMargin],
[self.vStackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.bottomVMargin],
[self.vStackView autoPinEdgeToSuperviewEdge:ALEdgeLeading
withInset:self.conversationStyle.fullWidthGutterLeading],
[self.vStackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
withInset:self.conversationStyle.fullWidthGutterTrailing],
];
}
@ -131,11 +207,10 @@ NS_ASSUME_NONNULL_BEGIN
return [UIColor ows_light60Color];
}
- (UIImage *)iconForInteraction:(TSInteraction *)interaction
- (nullable UIImage *)iconForInteraction:(TSInteraction *)interaction
{
UIImage *result = nil;
// TODO: Don't cast.
if ([interaction isKindOfClass:[TSErrorMessage class]]) {
switch (((TSErrorMessage *)interaction).errorType) {
case TSErrorMessageNonBlockingIdentityChange:
@ -150,8 +225,7 @@ NS_ASSUME_NONNULL_BEGIN
case TSErrorMessageInvalidVersion:
case TSErrorMessageUnknownContactBlockOffer:
case TSErrorMessageGroupCreationFailed:
result = [UIImage imageNamed:@"system_message_info"];
break;
return nil;
}
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
switch (((TSInfoMessage *)interaction).messageType) {
@ -161,30 +235,14 @@ NS_ASSUME_NONNULL_BEGIN
case TSInfoMessageAddToContactsOffer:
case TSInfoMessageAddUserToProfileWhitelistOffer:
case TSInfoMessageAddGroupToProfileWhitelistOffer:
result = [UIImage imageNamed:@"system_message_info"];
break;
case TSInfoMessageTypeGroupUpdate:
case TSInfoMessageTypeGroupQuit:
result = [UIImage imageNamed:@"system_message_group"];
break;
case TSInfoMessageTypeDisappearingMessagesUpdate:
result = [UIImage imageNamed:@"ic_timer"];
break;
case TSInfoMessageVerificationStateChange:
result = [UIImage imageNamed:@"system_message_verified"];
OWSAssert([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]);
if ([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
OWSVerificationStateChangeMessage *message = (OWSVerificationStateChangeMessage *)interaction;
BOOL isVerified = message.verificationState == OWSVerificationStateVerified;
if (!isVerified) {
result = [UIImage imageNamed:@"system_message_info"];
}
}
break;
return nil;
}
} else if ([interaction isKindOfClass:[TSCall class]]) {
result = [UIImage imageNamed:@"system_message_call"];
return nil;
} else {
OWSFail(@"Unknown interaction type: %@", [interaction class]);
return nil;
@ -268,9 +326,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(self.conversationStyle);
OWSAssert(self.viewItem);
CGFloat hMargins = (self.conversationStyle.fullWidthGutterLeading + self.conversationStyle.fullWidthGutterTrailing);
CGFloat maxTitleWidth
= (CGFloat)floor(self.conversationStyle.fullWidthContentWidth - (hMargins + self.iconSize + self.hSpacing));
CGFloat maxTitleWidth = (CGFloat)floor(self.conversationStyle.fullWidthContentWidth);
return [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)];
}
@ -283,11 +339,21 @@ NS_ASSUME_NONNULL_BEGIN
CGSize result = CGSizeMake(self.conversationStyle.viewWidth, 0);
[self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction];
UIImage *_Nullable icon = [self iconForInteraction:interaction];
if (icon) {
result.height += self.iconSize + self.iconVSpacing;
}
[self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction];
CGSize titleSize = [self titleSize];
CGFloat contentHeight = ceil(MAX([self iconSize], titleSize.height));
result.height = (contentHeight + self.topVMargin + self.bottomVMargin);
result.height += titleSize.height;
SystemMessageAction *_Nullable action = [self actionForInteraction:interaction];
if (action) {
result.height += self.buttonHeight + self.buttonVSpacing;
}
result.height += self.topVMargin + self.bottomVMargin;
return result;
}
@ -334,19 +400,153 @@ NS_ASSUME_NONNULL_BEGIN
return YES;
}
#pragma mark - Gesture recognizers
#pragma mark - Actions
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
- (nullable SystemMessageAction *)actionForInteraction:(TSInteraction *)interaction
{
OWSAssert(self.delegate);
OWSAssertIsOnMainThread();
OWSAssert(interaction);
if ([interaction isKindOfClass:[TSErrorMessage class]]) {
return [self actionForErrorMessage:(TSErrorMessage *)interaction];
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
return [self actionForInfoMessage:(TSInfoMessage *)interaction];
} else if ([interaction isKindOfClass:[TSCall class]]) {
return [self actionForCall:(TSCall *)interaction];
} else {
OWSFail(@"Tap for system messages of unknown type: %@", [interaction class]);
return nil;
}
}
- (nullable SystemMessageAction *)actionForErrorMessage:(TSErrorMessage *)message
{
OWSAssert(message);
__weak OWSSystemMessageCell *weakSelf = self;
switch (message.errorType) {
case TSErrorMessageInvalidKeyException:
return nil;
case TSErrorMessageNonBlockingIdentityChange:
return [SystemMessageAction
actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER",
@"Label for button to verify a user's safety number.")
block:^{
[weakSelf.delegate tappedNonBlockingIdentityChangeForRecipientId:message.recipientId];
}];
case TSErrorMessageWrongTrustedIdentityKey:
return [SystemMessageAction
actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER",
@"Label for button to verify a user's safety number.")
block:^{
[weakSelf.delegate
tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)message];
}];
case TSErrorMessageMissingKeyId:
case TSErrorMessageNoSession:
return nil;
case TSErrorMessageInvalidMessage:
return [SystemMessageAction actionWithTitle:NSLocalizedString(@"FINGERPRINT_SHRED_KEYMATERIAL_BUTTON", @"")
block:^{
[weakSelf.delegate tappedCorruptedMessage:message];
}];
case TSErrorMessageDuplicateMessage:
case TSErrorMessageInvalidVersion:
return nil;
case TSErrorMessageUnknownContactBlockOffer:
OWSFail(@"TSErrorMessageUnknownContactBlockOffer");
return nil;
case TSErrorMessageGroupCreationFailed:
return [SystemMessageAction actionWithTitle:CommonStrings.retryButton
block:^{
[weakSelf.delegate resendGroupUpdateForErrorMessage:message];
}];
}
DDLogWarn(@"%@ Unhandled tap for error message:%@", self.logTag, message);
}
if (sender.state == UIGestureRecognizerStateRecognized) {
TSInteraction *interaction = self.viewItem.interaction;
OWSAssert(interaction);
[self.delegate didTapSystemMessageWithInteraction:interaction];
- (nullable SystemMessageAction *)actionForInfoMessage:(TSInfoMessage *)message
{
OWSAssert(message);
__weak OWSSystemMessageCell *weakSelf = self;
switch (message.messageType) {
case TSInfoMessageUserNotRegistered:
case TSInfoMessageTypeSessionDidEnd:
return nil;
case TSInfoMessageTypeUnsupportedMessage:
// Unused.
return nil;
case TSInfoMessageAddToContactsOffer:
// Unused.
OWSFail(@"TSInfoMessageAddToContactsOffer");
return nil;
case TSInfoMessageAddUserToProfileWhitelistOffer:
// Unused.
OWSFail(@"TSInfoMessageAddUserToProfileWhitelistOffer");
return nil;
case TSInfoMessageAddGroupToProfileWhitelistOffer:
// Unused.
OWSFail(@"TSInfoMessageAddGroupToProfileWhitelistOffer");
return nil;
case TSInfoMessageTypeGroupUpdate:
return [SystemMessageAction
actionWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS", @"title for conversation settings screen")
block:^{
[weakSelf.delegate showConversationSettings];
}];
case TSInfoMessageTypeGroupQuit:
return nil;
case TSInfoMessageTypeDisappearingMessagesUpdate:
return [SystemMessageAction
actionWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS", @"title for conversation settings screen")
block:^{
[weakSelf.delegate showConversationSettings];
}];
case TSInfoMessageVerificationStateChange:
return [SystemMessageAction
actionWithTitle:NSLocalizedString(@"SHOW_SAFETY_NUMBER_ACTION", @"Action sheet item")
block:^{
[weakSelf.delegate
showFingerprintWithRecipientId:((OWSVerificationStateChangeMessage *)message)
.recipientId];
}];
}
DDLogInfo(@"%@ Unhandled tap for info message:%@", self.logTag, message);
}
- (nullable SystemMessageAction *)actionForCall:(TSCall *)call
{
OWSAssert(call);
__weak OWSSystemMessageCell *weakSelf = self;
switch (call.callType) {
case RPRecentCallTypeIncoming:
case RPRecentCallTypeIncomingMissed:
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
case RPRecentCallTypeIncomingDeclined:
return
[SystemMessageAction actionWithTitle:NSLocalizedString(@"CALLBACK_BUTTON_TITLE", @"notification action")
block:^{
[weakSelf.delegate handleCallTap:call];
}];
case RPRecentCallTypeOutgoing:
case RPRecentCallTypeOutgoingMissed:
return [SystemMessageAction actionWithTitle:NSLocalizedString(@"CALL_AGAIN_BUTTON_TITLE",
@"Label for button that lets users call a contact again.")
block:^{
[weakSelf.delegate handleCallTap:call];
}];
case RPRecentCallTypeOutgoingIncomplete:
case RPRecentCallTypeIncomingIncomplete:
return nil;
}
}
#pragma mark - Events
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPress
{
OWSAssert(self.delegate);
@ -359,6 +559,24 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (void)buttonWasPressed:(id)sender
{
if (!self.action.block) {
OWSFail(@"%@ Missing action", self.logTag);
} else {
self.action.block();
}
}
#pragma mark - Reuse
- (void)prepareForReuse
{
[super prepareForReuse];
self.action = nil;
}
@end
NS_ASSUME_NONNULL_END

@ -1863,45 +1863,6 @@ typedef enum : NSUInteger {
[self presentViewController:actionSheetController animated:YES completion:nil];
}
- (void)handleErrorMessageTap:(TSErrorMessage *)message
{
OWSAssert(message);
switch (message.errorType) {
case TSErrorMessageInvalidKeyException:
break;
case TSErrorMessageNonBlockingIdentityChange:
[self tappedNonBlockingIdentityChangeForRecipientId:message.recipientId];
return;
case TSErrorMessageWrongTrustedIdentityKey:
OWSAssert([message isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]);
[self tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)message];
return;
case TSErrorMessageMissingKeyId:
// Unused.
break;
case TSErrorMessageNoSession:
break;
case TSErrorMessageInvalidMessage:
[self tappedCorruptedMessage:message];
return;
case TSErrorMessageDuplicateMessage:
// Unused.
break;
case TSErrorMessageInvalidVersion:
break;
case TSErrorMessageUnknownContactBlockOffer:
// Unused.
OWSFail(@"TSErrorMessageUnknownContactBlockOffer");
return;
case TSErrorMessageGroupCreationFailed:
[self resendGroupUpdateForErrorMessage:message];
return;
}
DDLogWarn(@"%@ Unhandled tap for error message:%@", self.logTag, message);
}
- (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalId
{
if (signalId == nil) {
@ -1920,46 +1881,6 @@ typedef enum : NSUInteger {
[self showFingerprintWithRecipientId:signalId];
}
- (void)handleInfoMessageTap:(TSInfoMessage *)message
{
OWSAssert(message);
switch (message.messageType) {
case TSInfoMessageUserNotRegistered:
break;
case TSInfoMessageTypeSessionDidEnd:
break;
case TSInfoMessageTypeUnsupportedMessage:
// Unused.
break;
case TSInfoMessageAddToContactsOffer:
// Unused.
OWSFail(@"TSInfoMessageAddToContactsOffer");
return;
case TSInfoMessageAddUserToProfileWhitelistOffer:
// Unused.
OWSFail(@"TSInfoMessageAddUserToProfileWhitelistOffer");
return;
case TSInfoMessageAddGroupToProfileWhitelistOffer:
// Unused.
OWSFail(@"TSInfoMessageAddGroupToProfileWhitelistOffer");
return;
case TSInfoMessageTypeGroupUpdate:
[self showConversationSettings];
return;
case TSInfoMessageTypeGroupQuit:
break;
case TSInfoMessageTypeDisappearingMessagesUpdate:
[self showConversationSettings];
return;
case TSInfoMessageVerificationStateChange:
[self showFingerprintWithRecipientId:((OWSVerificationStateChangeMessage *)message).recipientId];
break;
}
DDLogInfo(@"%@ Unhandled tap for info message:%@", self.logTag, message);
}
- (void)tappedCorruptedMessage:(TSErrorMessage *)message
{
NSString *alertMessage = [NSString
@ -2518,24 +2439,6 @@ typedef enum : NSUInteger {
[self.inputToolbar beginEditingTextMessage];
}
#pragma mark - System Messages
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction
{
OWSAssertIsOnMainThread();
OWSAssert(interaction);
if ([interaction isKindOfClass:[TSErrorMessage class]]) {
[self handleErrorMessageTap:(TSErrorMessage *)interaction];
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
[self handleInfoMessageTap:(TSInfoMessage *)interaction];
} else if ([interaction isKindOfClass:[TSCall class]]) {
[self handleCallTap:(TSCall *)interaction];
} else {
OWSFail(@"Tap for system messages of unknown type: %@", [interaction class]);
}
}
#pragma mark - ContactEditingDelegate
- (void)didFinishEditingContact

@ -3411,11 +3411,11 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
inThread:contactThread]];
[result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
withCallNumber:@"+19174054215"
callType:RPRecentCallTypeMissed
callType:RPRecentCallTypeIncomingMissed
inThread:contactThread]];
[result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
withCallNumber:@"+19174054215"
callType:RPRecentCallTypeMissedBecauseOfChangedIdentity
callType:RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity
inThread:contactThread]];
[result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
withCallNumber:@"+19174054215"
@ -3425,6 +3425,10 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
withCallNumber:@"+19174054215"
callType:RPRecentCallTypeIncomingIncomplete
inThread:contactThread]];
[result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
withCallNumber:@"+19174054215"
callType:RPRecentCallTypeOutgoingMissed
inThread:contactThread]];
}
{

@ -515,12 +515,12 @@ private class SignalCallData: NSObject {
// Insert missed call record
if let callRecord = call.callRecord {
if callRecord.callType == RPRecentCallTypeIncoming {
callRecord.updateCallType(RPRecentCallTypeMissed)
callRecord.updateCallType(RPRecentCallTypeIncomingMissed)
}
} else {
call.callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(),
withCallNumber: call.thread.contactIdentifier(),
callType: RPRecentCallTypeMissed,
callType: RPRecentCallTypeIncomingMissed,
in: call.thread)
}
@ -602,7 +602,7 @@ private class SignalCallData: NSObject {
let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(),
withCallNumber: thread.contactIdentifier(),
callType: RPRecentCallTypeMissedBecauseOfChangedIdentity,
callType: RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity,
in: thread)
assert(newCall.callRecord == nil)
newCall.callRecord = callRecord
@ -1107,6 +1107,14 @@ private class SignalCallData: NSObject {
call.state = .localHangup
if let callRecord = call.callRecord {
if callRecord.callType == RPRecentCallTypeOutgoingIncomplete {
callRecord.updateCallType(RPRecentCallTypeOutgoingMissed)
}
} else {
owsFail("\(self.logTag) missing call record in \(#function)")
}
// TODO something like this lifted from Signal-Android.
// this.accountManager.cancelInFlightRequests();
// this.messageSender.cancelInFlightRequests();

@ -284,6 +284,9 @@
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Select";
/* Label for button that lets users call a contact again. */
"CALL_AGAIN_BUTTON_TITLE" = "Call Again";
/* Alert message when calling and permissions for microphone are missing */
"CALL_AUDIO_PERMISSION_MESSAGE" = "Signal requires access to your microphone to make calls and record voice messages. You can grant this permission in the Settings app.";
@ -871,7 +874,7 @@
"ERROR_MESSAGE_INVALID_KEY_EXCEPTION" = "The recipient's key is not valid.";
/* No comment provided by engineer. */
"ERROR_MESSAGE_INVALID_MESSAGE" = "Received message was out of sync. Tap to reset your secure session.";
"ERROR_MESSAGE_INVALID_MESSAGE" = "Received message was out of sync.";
/* No comment provided by engineer. */
"ERROR_MESSAGE_INVALID_VERSION" = "Received a message not compatible with this version.";
@ -889,7 +892,7 @@
"ERROR_MESSAGE_UNKNOWN_ERROR" = "An unknown error occurred.";
/* No comment provided by engineer. */
"ERROR_MESSAGE_WRONG_TRUSTED_IDENTITY_KEY" = "Safety number changed. Tap to verify.";
"ERROR_MESSAGE_WRONG_TRUSTED_IDENTITY_KEY" = "Safety number changed.";
/* Format string for 'unregistered user' error. Embeds {{the unregistered user's name or signal id}}. */
"ERROR_UNREGISTERED_USER_FORMAT" = "Unregistered User: %@";
@ -1054,7 +1057,7 @@
"INCOMING_DECLINED_CALL" = "You declined a call";
/* No comment provided by engineer. */
"INCOMING_INCOMPLETE_CALL" = "Incomplete incoming call from";
"INCOMING_INCOMPLETE_CALL" = "Incoming call";
/* info message text shown in conversation view */
"INFO_MESSAGE_MISSED_CALL_DUE_TO_CHANGED_IDENITY" = "Missed call because their safety number has changed.";
@ -1425,7 +1428,10 @@
"OUTGOING_CALL" = "Outgoing call";
/* No comment provided by engineer. */
"OUTGOING_INCOMPLETE_CALL" = "Unanswered outgoing call";
"OUTGOING_INCOMPLETE_CALL" = "Outgoing call";
/* info message recorded in conversation history when local user tries and fails to call another user. */
"OUTGOING_MISSED_CALL" = "Unanswered outgoing call";
/* A display format for oversize text messages. */
"OVERSIZE_TEXT_DISPLAY_FORMAT" = "%@…";
@ -2120,6 +2126,9 @@
/* No comment provided by engineer. */
"SUCCESSFUL_VERIFICATION_TITLE" = "Safety Number Matches!";
/* Label for button to verify a user's safety number. */
"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER" = "Verify Safety Number";
/* {{number of days}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 days}}'. See other *_TIME_AMOUNT strings */
"TIME_AMOUNT_DAYS" = "%@ days";

@ -0,0 +1,29 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class OWSPrimaryStorage;
@class OWSStorage;
@interface OWSIncompleteCallsJob : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;
- (void)run;
+ (NSString *)databaseExtensionName;
+ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage;
#ifdef DEBUG
/**
* Only use the sync version for testing, generally we'll want to register extensions async
*/
- (void)blockingRegisterDatabaseExtensions;
#endif
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,150 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSIncompleteCallsJob.h"
#import "OWSPrimaryStorage.h"
#import "TSCall.h"
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseQuery.h>
#import <YapDatabase/YapDatabaseSecondaryIndex.h>
NS_ASSUME_NONNULL_BEGIN
static NSString *const OWSIncompleteCallsJobCallTypeColumn = @"call_type";
static NSString *const OWSIncompleteCallsJobCallTypeIndex = @"index_calls_on_call_type";
@interface OWSIncompleteCallsJob ()
@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage;
@end
#pragma mark -
@implementation OWSIncompleteCallsJob
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
{
self = [super init];
if (!self) {
return self;
}
_primaryStorage = primaryStorage;
return self;
}
- (NSArray<NSString *> *)fetchIncompleteCallIdsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
NSMutableArray<NSString *> *messageIds = [NSMutableArray new];
NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ == %d OR %@ == %d",
OWSIncompleteCallsJobCallTypeColumn,
(int)RPRecentCallTypeOutgoingIncomplete,
OWSIncompleteCallsJobCallTypeColumn,
(int)RPRecentCallTypeIncomingIncomplete];
YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString];
[[transaction ext:OWSIncompleteCallsJobCallTypeIndex]
enumerateKeysMatchingQuery:query
usingBlock:^void(NSString *collection, NSString *key, BOOL *stop) {
[messageIds addObject:key];
}];
return [messageIds copy];
}
- (void)enumerateIncompleteCallsWithBlock:(void (^)(TSCall *call))block
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
// Since we can't directly mutate the enumerated "incomplete" calls, we store only their ids in hopes
// of saving a little memory and then enumerate the (larger) TSCall objects one at a time.
for (NSString *callId in [self fetchIncompleteCallIdsWithTransaction:transaction]) {
TSCall *_Nullable call = [TSCall fetchObjectWithUniqueID:callId transaction:transaction];
if ([call isKindOfClass:[TSCall class]]) {
block(call);
} else {
DDLogError(@"%@ unexpected object: %@", self.logTag, call);
}
}
}
- (void)run
{
__block uint count = 0;
[[self.primaryStorage newDatabaseConnection] readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self
enumerateIncompleteCallsWithBlock:^(TSCall *call) {
if (call.callType == RPRecentCallTypeOutgoingIncomplete) {
DDLogDebug(@"%@ marking call as missed: %@", self.logTag, call.uniqueId);
[call updateCallType:RPRecentCallTypeOutgoingMissed transaction:transaction];
OWSAssert(call.callType == RPRecentCallTypeOutgoingMissed);
} else if (call.callType == RPRecentCallTypeIncomingIncomplete) {
DDLogDebug(@"%@ marking call as missed: %@", self.logTag, call.uniqueId);
[call updateCallType:RPRecentCallTypeIncomingMissed transaction:transaction];
OWSAssert(call.callType == RPRecentCallTypeIncomingMissed);
} else {
OWSProdLogAndFail(
@"%@ call has unexpected call type: %@", self.logTag, NSStringFromCallType(call.callType));
return;
}
count++;
}
transaction:transaction];
}];
DDLogDebug(@"%@ Marked %u calls as missed", self.logTag, count);
}
#pragma mark - YapDatabaseExtension
+ (YapDatabaseSecondaryIndex *)indexDatabaseExtension
{
YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new];
[setup addColumn:OWSIncompleteCallsJobCallTypeColumn withType:YapDatabaseSecondaryIndexTypeInteger];
YapDatabaseSecondaryIndexHandler *handler =
[YapDatabaseSecondaryIndexHandler withObjectBlock:^(YapDatabaseReadTransaction *transaction,
NSMutableDictionary *dict,
NSString *collection,
NSString *key,
id object) {
if (![object isKindOfClass:[TSCall class]]) {
return;
}
TSCall *call = (TSCall *)object;
dict[OWSIncompleteCallsJobCallTypeColumn] = @(call.callType);
}];
return [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler versionTag:nil];
}
#ifdef DEBUG
// Useful for tests, don't use in app startup path because it's slow.
- (void)blockingRegisterDatabaseExtensions
{
[self.primaryStorage registerExtension:[self.class indexDatabaseExtension]
withName:OWSIncompleteCallsJobCallTypeIndex];
}
#endif
+ (NSString *)databaseExtensionName
{
return OWSIncompleteCallsJobCallTypeIndex;
}
+ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage
{
[storage asyncRegisterExtension:[self indexDatabaseExtension] withName:OWSIncompleteCallsJobCallTypeIndex];
}
@end
NS_ASSUME_NONNULL_END

@ -12,14 +12,17 @@ NS_ASSUME_NONNULL_BEGIN
typedef enum {
RPRecentCallTypeIncoming = 1,
RPRecentCallTypeOutgoing,
RPRecentCallTypeMissed,
RPRecentCallTypeIncomingMissed,
// These call types are used until the call connects.
RPRecentCallTypeOutgoingIncomplete,
RPRecentCallTypeIncomingIncomplete,
RPRecentCallTypeMissedBecauseOfChangedIdentity,
RPRecentCallTypeIncomingDeclined
RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity,
RPRecentCallTypeIncomingDeclined,
RPRecentCallTypeOutgoingMissed,
} RPRecentCallType;
NSString *NSStringFromCallType(RPRecentCallType callType);
@interface TSCall : TSInteraction <OWSReadTracking, OWSPreviewText>
@property (nonatomic, readonly) RPRecentCallType callType;
@ -34,6 +37,7 @@ typedef enum {
- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
- (void)updateCallType:(RPRecentCallType)callType;
- (void)updateCallType:(RPRecentCallType)callType transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end

@ -9,6 +9,28 @@
NS_ASSUME_NONNULL_BEGIN
NSString *NSStringFromCallType(RPRecentCallType callType)
{
switch (callType) {
case RPRecentCallTypeIncoming:
return @"RPRecentCallTypeIncoming";
case RPRecentCallTypeOutgoing:
return @"RPRecentCallTypeOutgoing";
case RPRecentCallTypeIncomingMissed:
return @"RPRecentCallTypeIncomingMissed";
case RPRecentCallTypeOutgoingIncomplete:
return @"RPRecentCallTypeOutgoingIncomplete";
case RPRecentCallTypeIncomingIncomplete:
return @"RPRecentCallTypeIncomingIncomplete";
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
return @"RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity";
case RPRecentCallTypeIncomingDeclined:
return @"RPRecentCallTypeIncomingDeclined";
case RPRecentCallTypeOutgoingMissed:
return @"RPRecentCallTypeOutgoingMissed";
}
}
NSUInteger TSCallCurrentSchemaVersion = 1;
@interface TSCall ()
@ -36,7 +58,11 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
_callSchemaVersion = TSCallCurrentSchemaVersion;
_callType = callType;
if (_callType == RPRecentCallTypeMissed || _callType == RPRecentCallTypeMissedBecauseOfChangedIdentity) {
// Ensure users are notified of missed calls.
BOOL isIncomingMissed = (_callType == RPRecentCallTypeIncomingMissed
|| _callType == RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity);
if (isIncomingMissed) {
_read = NO;
} else {
_read = YES;
@ -75,17 +101,20 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
return NSLocalizedString(@"INCOMING_CALL", @"");
case RPRecentCallTypeOutgoing:
return NSLocalizedString(@"OUTGOING_CALL", @"");
case RPRecentCallTypeMissed:
case RPRecentCallTypeIncomingMissed:
return NSLocalizedString(@"MISSED_CALL", @"");
case RPRecentCallTypeOutgoingIncomplete:
return NSLocalizedString(@"OUTGOING_INCOMPLETE_CALL", @"");
case RPRecentCallTypeIncomingIncomplete:
return NSLocalizedString(@"INCOMING_INCOMPLETE_CALL", @"");
case RPRecentCallTypeMissedBecauseOfChangedIdentity:
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
return NSLocalizedString(@"INFO_MESSAGE_MISSED_CALL_DUE_TO_CHANGED_IDENITY", @"info message text shown in conversation view");
case RPRecentCallTypeIncomingDeclined:
return NSLocalizedString(@"INCOMING_DECLINED_CALL",
@"info message recorded in conversation history when local user declined a call");
case RPRecentCallTypeOutgoingMissed:
return NSLocalizedString(@"OUTGOING_MISSED_CALL",
@"info message recorded in conversation history when local user tries and fails to call another user.");
}
}
@ -125,20 +154,28 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
- (void)updateCallType:(RPRecentCallType)callType
{
DDLogInfo(@"%@ updating call type of call: %d with uniqueId: %@ which has timestamp: %llu",
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self updateCallType:callType transaction:transaction];
}];
}
- (void)updateCallType:(RPRecentCallType)callType transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(transaction);
DDLogInfo(@"%@ updating call type of call: %@ -> %@ with uniqueId: %@ which has timestamp: %llu",
self.logTag,
(int)self.callType,
NSStringFromCallType(_callType),
NSStringFromCallType(callType),
self.uniqueId,
self.timestamp);
_callType = callType;
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
[self saveWithTransaction:transaction];
// redraw any thread-related unread count UI.
[self touchThreadWithTransaction:transaction];
}];
[self saveWithTransaction:transaction];
// redraw any thread-related unread count UI.
[self touchThreadWithTransaction:transaction];
}
@end

@ -11,6 +11,7 @@
#import "OWSFailedMessagesJob.h"
#import "OWSFileSystem.h"
#import "OWSIncomingMessageFinder.h"
#import "OWSIncompleteCallsJob.h"
#import "OWSMediaGalleryFinder.h"
#import "OWSMessageReceiver.h"
#import "OWSStorage+Subclass.h"
@ -64,9 +65,10 @@ void RunAsyncRegistrationsForStorage(OWSStorage *storage, dispatch_block_t compl
[TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView:storage];
[OWSDisappearingMessagesFinder asyncRegisterDatabaseExtensions:storage];
[OWSFailedMessagesJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage];
[OWSIncompleteCallsJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage];
[OWSFailedAttachmentDownloadsJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage];
[OWSMediaGalleryFinder asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage];
// NOTE: Always pass the completion to the _LAST_ of the async database
// view registrations.
[TSDatabaseView asyncRegisterLazyRestoreAttachmentsDatabaseView:storage completion:completion];

@ -234,7 +234,11 @@ NSString *const kNSUserDefaults_DatabaseExtensionVersionMap = @"kNSUserDefaults_
cannotDecodeObjectOfClassName:(NSString *)name
originalClasses:(NSArray<NSString *> *)classNames
{
OWSProdLogAndFail(@"%@ Could not decode object: %@", self.logTag, name);
if ([name isEqualToString:@"TSRecipient"]) {
DDLogError(@"%@ Could not decode object: %@", self.logTag, name);
} else {
OWSProdLogAndFail(@"%@ Could not decode object: %@", self.logTag, name);
}
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotDecodeClass]);
return [OWSUnknownDBObject class];
}

Loading…
Cancel
Save