Merge branch 'charlesmchen/tweakSystemMessages2'

pull/1/head
Matthew Chen 7 years ago
commit bbe2760481

@ -43,7 +43,6 @@
340FC8D0205BF2FA007AEB0F /* OWSBackupIO.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8CE205BF2FA007AEB0F /* OWSBackupIO.m */; };
341F2C0F1F2B8AE700D07D6B /* DebugUIMisc.m in Sources */ = {isa = PBXBuildFile; fileRef = 341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */; };
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34277A5C20751BDC006049F2 /* OWSQuotedMessageView.m */; };
3427C64020EFD43E00EEC730 /* OWSCallMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 3427C63E20EFD43D00EEC730 /* OWSCallMessageCell.m */; };
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; };
34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; };
@ -642,8 +641,6 @@
341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMisc.m; sourceTree = "<group>"; };
34277A5C20751BDC006049F2 /* OWSQuotedMessageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSQuotedMessageView.m; sourceTree = "<group>"; };
34277A5D20751BDC006049F2 /* OWSQuotedMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQuotedMessageView.h; sourceTree = "<group>"; };
3427C63E20EFD43D00EEC730 /* OWSCallMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSCallMessageCell.m; sourceTree = "<group>"; };
3427C63F20EFD43E00EEC730 /* OWSCallMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSCallMessageCell.h; sourceTree = "<group>"; };
3430FE171F7751D4000EC51B /* GiphyAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyAPI.swift; sourceTree = "<group>"; };
34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "fontawesome-webfont.ttf"; sourceTree = "<group>"; };
34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dripicons-v2.ttf"; sourceTree = "<group>"; };
@ -1758,8 +1755,6 @@
34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */,
34DBF002206BD5A500025978 /* OWSBubbleView.h */,
34DBF001206BD5A500025978 /* OWSBubbleView.m */,
3427C63F20EFD43E00EEC730 /* OWSCallMessageCell.h */,
3427C63E20EFD43D00EEC730 /* OWSCallMessageCell.m */,
34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */,
34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */,
3403B95C20EA9527001A1F44 /* OWSContactShareButtonsView.h */,
@ -3209,7 +3204,6 @@
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */,
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
3427C64020EFD43E00EEC730 /* OWSCallMessageCell.m in Sources */,
45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */,
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */,

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

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

@ -12,11 +12,15 @@ NS_ASSUME_NONNULL_BEGIN
@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>
@ -26,14 +30,15 @@ NS_ASSUME_NONNULL_BEGIN
- (void)showMetadataViewForViewItem:(ConversationViewItem *)conversationItem;
- (void)conversationCell:(ConversationViewCell *)cell didTapReplyForViewItem:(ConversationViewItem *)conversationItem;
#pragma mark - Calls
- (void)didTapCall:(TSCall *)call;
#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

@ -1,17 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewCell.h"
NS_ASSUME_NONNULL_BEGIN
@class TSInteraction;
@interface OWSCallMessageCell : ConversationViewCell
+ (NSString *)cellReuseIdentifier;
@end
NS_ASSUME_NONNULL_END

@ -1,386 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSCallMessageCell.h"
#import "ConversationViewItem.h"
#import "OWSBubbleView.h"
#import "OWSMessageFooterView.h"
#import "Signal-Swift.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SignalMessaging/Environment.h>
#import <SignalMessaging/OWSContactsManager.h>
#import <SignalServiceKit/OWSVerificationStateChangeMessage.h>
#import <SignalServiceKit/TSCall.h>
#import <SignalServiceKit/TSErrorMessage.h>
#import <SignalServiceKit/TSInfoMessage.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSCallMessageCell ()
@property (nonatomic, nullable) TSInteraction *interaction;
@property (nonatomic) OWSBubbleView *bubbleView;
@property (nonatomic) UIImageView *imageView;
@property (nonatomic) UIView *circleView;
@property (nonatomic) UILabel *titleLabel;
@property (nonatomic) OWSMessageFooterView *footerView;
@property (nonatomic) UIStackView *hStackView;
@property (nonatomic) UIStackView *vStackView;
@property (nonatomic) NSMutableArray<NSLayoutConstraint *> *layoutConstraints;
@end
#pragma mark -
@implementation OWSCallMessageCell
// `[UIView init]` invokes `[self initWithFrame:...]`.
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commontInit];
}
return self;
}
- (void)commontInit
{
OWSAssert(!self.imageView);
self.layoutMargins = UIEdgeInsetsZero;
self.contentView.layoutMargins = UIEdgeInsetsZero;
self.layoutConstraints = [NSMutableArray new];
self.bubbleView = [OWSBubbleView new];
self.bubbleView.userInteractionEnabled = NO;
[self.contentView addSubview:self.bubbleView];
[self.bubbleView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.bubbleView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
self.imageView = [UIImageView new];
[self.imageView setContentHuggingHigh];
self.circleView = [UIView new];
self.circleView.backgroundColor = [UIColor whiteColor];
self.circleView.layer.cornerRadius = self.circleSize * 0.5f;
[self.circleView addSubview:self.imageView];
[self.imageView autoCenterInSuperview];
[self.circleView autoSetDimension:ALDimensionWidth toSize:self.circleSize];
[self.circleView autoSetDimension:ALDimensionHeight toSize:self.circleSize];
[self.circleView setContentHuggingHigh];
self.titleLabel = [UILabel new];
self.titleLabel.numberOfLines = 0;
self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
[self.titleLabel setContentHuggingLow];
self.hStackView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.circleView,
self.titleLabel,
]];
self.hStackView.axis = UILayoutConstraintAxisHorizontal;
self.hStackView.spacing = self.hSpacing;
self.hStackView.alignment = UIStackViewAlignmentCenter;
self.footerView = [OWSMessageFooterView new];
self.vStackView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.hStackView,
self.footerView,
]];
self.vStackView.axis = UILayoutConstraintAxisVertical;
self.vStackView.spacing = self.vSpacing;
self.vStackView.userInteractionEnabled = NO;
[self.bubbleView addSubview:self.vStackView];
[self.vStackView autoPinToSuperviewEdges];
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
[self addGestureRecognizer:tap];
UILongPressGestureRecognizer *longPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
[self addGestureRecognizer:longPress];
}
- (void)configureFonts
{
// Update cell to reflect changes in dynamic text.
self.titleLabel.font = UIFont.ows_dynamicTypeSubheadlineFont;
}
+ (NSString *)cellReuseIdentifier
{
return NSStringFromClass([self class]);
}
- (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(self.conversationStyle);
OWSAssert(self.viewItem);
OWSAssert([self.viewItem.interaction isKindOfClass:[TSCall class]]);
TSCall *call = (TSCall *)self.viewItem.interaction;
self.bubbleView.bubbleColor = [self bubbleColorForCall:call];
UIImage *icon = [self iconForCall:call];
self.imageView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
self.imageView.tintColor = [self iconColorForCall:call];
self.titleLabel.textColor = [self textColorForCall:call];
[self applyTitleForCall:call label:self.titleLabel];
if (self.hasFooter) {
[self.footerView configureWithConversationViewItem:self.viewItem
isOverlayingMedia:NO
conversationStyle:self.conversationStyle
isIncoming:call.isIncoming];
self.footerView.hidden = NO;
} else {
self.footerView.hidden = YES;
}
if (call.isIncoming) {
[self.layoutConstraints addObjectsFromArray:@[
[self.bubbleView autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:self.conversationStyle.gutterLeading],
[self.bubbleView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
withInset:self.conversationStyle.gutterTrailing
relation:NSLayoutRelationGreaterThanOrEqual],
]];
} else {
[self.layoutConstraints addObjectsFromArray:@[
[self.bubbleView autoPinEdgeToSuperviewEdge:ALEdgeLeading
withInset:self.conversationStyle.gutterLeading
relation:NSLayoutRelationGreaterThanOrEqual],
[self.bubbleView autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:self.conversationStyle.gutterTrailing],
]];
}
CGSize cellSize = [self cellSizeWithTransaction:transaction];
[self.layoutConstraints addObjectsFromArray:[self.bubbleView autoSetDimensionsToSize:cellSize]];
self.vStackView.layoutMarginsRelativeArrangement = YES;
self.vStackView.layoutMargins = UIEdgeInsetsMake(self.conversationStyle.textInsetTop,
self.conversationStyle.textInsetHorizontal,
self.conversationStyle.textInsetBottom,
self.conversationStyle.textInsetHorizontal);
}
- (BOOL)hasFooter
{
return !self.viewItem.shouldHideFooter;
}
- (CGFloat)circleSize
{
return 48.f;
}
- (UIColor *)textColorForCall:(TSCall *)call
{
return [self.conversationStyle bubbleTextColorWithCall:call];
}
- (UIColor *)bubbleColorForCall:(TSCall *)call
{
return [self.conversationStyle bubbleColorWithCall:call];
}
- (UIColor *)iconColorForCall:(TSCall *)call
{
switch (call.callType) {
case RPRecentCallTypeIncoming:
case RPRecentCallTypeOutgoing:
case RPRecentCallTypeIncomingIncomplete:
case RPRecentCallTypeOutgoingIncomplete:
return [UIColor ows_greenColor];
case RPRecentCallTypeIncomingMissed:
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
case RPRecentCallTypeIncomingDeclined:
return [UIColor ows_redColor];
}
}
- (UIImage *)iconForCall:(TSCall *)call
{
UIImage *result = nil;
switch (call.callType) {
case RPRecentCallTypeIncoming:
case RPRecentCallTypeOutgoing:
case RPRecentCallTypeIncomingIncomplete:
case RPRecentCallTypeOutgoingIncomplete:
result = [UIImage imageNamed:@"phone-up"];
break;
case RPRecentCallTypeIncomingMissed:
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
case RPRecentCallTypeIncomingDeclined:
result = [UIImage imageNamed:@"phone-down"];
break;
}
OWSAssert(result);
return result;
}
- (void)applyTitleForCall:(TSCall *)call label:(UILabel *)label
{
OWSAssert(call);
OWSAssert(label);
[self configureFonts];
label.text = [self titleForCall:call];
}
- (NSString *)titleForCall:(TSCall *)call
{
// We don't actually use the `transaction` but other sibling classes do.
switch (call.callType) {
case RPRecentCallTypeIncoming:
case RPRecentCallTypeOutgoing:
case RPRecentCallTypeOutgoingIncomplete:
case RPRecentCallTypeIncomingIncomplete:
return NSLocalizedString(@"CALL_DEFAULT_STATUS",
@"Message recorded in conversation history when local user is making or has completed a call.");
case RPRecentCallTypeIncomingMissed:
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
return NSLocalizedString(
@"CALL_MISSED", @"Message recorded in conversation history when local user missed a call.");
case RPRecentCallTypeIncomingDeclined:
return NSLocalizedString(
@"CALL_DECLINED", @"Message recorded in conversation history when local user declined a call.");
}
}
- (CGFloat)hSpacing
{
return 8.f;
}
- (CGFloat)vSpacing
{
return 6.f;
}
- (CGSize)titleSize
{
OWSAssert(self.conversationStyle);
OWSAssert(self.viewItem);
CGFloat maxTitleWidth = (CGFloat)ceil(self.conversationStyle.maxMessageWidth
- (self.circleSize + self.hSpacing + self.conversationStyle.textInsetHorizontal * 2));
DDLogVerbose(@"%@ maxTitleWidth %f", self.logTag, maxTitleWidth);
return [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)];
}
- (CGSize)cellSizeWithTransaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(self.conversationStyle);
OWSAssert(self.viewItem);
OWSAssert([self.viewItem.interaction isKindOfClass:[TSCall class]]);
TSCall *call = (TSCall *)self.viewItem.interaction;
[self applyTitleForCall:call label:self.titleLabel];
CGSize titleSize = [self titleSize];
CGSize hStackSize = titleSize;
hStackSize.width += (self.hSpacing + self.circleSize);
hStackSize.height = MAX(hStackSize.height, self.circleSize);
CGSize vStackSize = hStackSize;
if (self.hasFooter) {
CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem];
vStackSize.height += (self.vSpacing + footerSize.height);
vStackSize.width = MAX(vStackSize.width, footerSize.width);
}
CGSize result = CGSizeCeil(CGSizeMake(
MIN(self.conversationStyle.viewWidth, vStackSize.width + self.conversationStyle.textInsetHorizontal * 2),
vStackSize.height + self.conversationStyle.textInsetTop + self.conversationStyle.textInsetBottom));
return result;
}
#pragma mark - UIMenuController
- (void)showMenuController
{
OWSAssertIsOnMainThread();
DDLogDebug(@"%@ long pressed call cell: %@", self.logTag, self.viewItem.interaction.debugDescription);
[self becomeFirstResponder];
if ([UIMenuController sharedMenuController].isMenuVisible) {
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
}
UIMenuController *menuController = [UIMenuController sharedMenuController];
menuController.menuItems = @[];
UIView *fromView = self.titleLabel;
CGRect targetRect = [fromView.superview convertRect:fromView.frame toView:self];
[menuController setTargetRect:targetRect inView:self];
[menuController setMenuVisible:YES animated:YES];
}
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
{
return action == @selector(delete:);
}
- (void) delete:(nullable id)sender
{
DDLogInfo(@"%@ chose delete", self.logTag);
TSInteraction *interaction = self.viewItem.interaction;
OWSAssert(interaction);
[interaction remove];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (void)prepareForReuse
{
[NSLayoutConstraint deactivateConstraints:self.layoutConstraints];
[self.layoutConstraints removeAllObjects];
[self.footerView prepareForReuse];
}
#pragma mark - Gesture recognizers
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
{
OWSAssert(self.delegate);
OWSAssert([self.viewItem.interaction isKindOfClass:[TSCall class]]);
if (sender.state == UIGestureRecognizerStateRecognized) {
TSCall *call = (TSCall *)self.viewItem.interaction;
[self.delegate didTapCall:call];
}
}
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPress
{
OWSAssert(self.delegate);
TSInteraction *interaction = self.viewItem.interaction;
OWSAssert(interaction);
if (longPress.state == UIGestureRecognizerStateBegan) {
[self showMenuController];
}
}
@end
NS_ASSUME_NONNULL_END

@ -11,19 +11,47 @@
#import <SignalMessaging/Environment.h>
#import <SignalMessaging/OWSContactsManager.h>
#import <SignalServiceKit/OWSVerificationStateChangeMessage.h>
#import <SignalServiceKit/TSCall.h>
#import <SignalServiceKit/TSErrorMessage.h>
#import <SignalServiceKit/TSInfoMessage.h>
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
@ -43,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.
@ -94,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],
];
}
@ -130,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:
@ -149,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) {
@ -160,28 +235,24 @@ 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;
return nil;
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"];
return nil;
}
}
result = [UIImage imageNamed:@"system_message_verified"];
break;
}
} else if ([interaction isKindOfClass:[TSCall class]]) {
return nil;
} else {
OWSFail(@"Unknown interaction type: %@", [interaction class]);
return nil;
@ -231,6 +302,9 @@ NS_ASSUME_NONNULL_BEGIN
} else {
label.text = [infoMessage previewTextWithTransaction:transaction];
}
} else if ([interaction isKindOfClass:[TSCall class]]) {
TSCall *call = (TSCall *)interaction;
label.text = [call previewTextWithTransaction:transaction];
} else {
OWSFail(@"Unknown interaction type: %@", [interaction class]);
label.text = nil;
@ -262,9 +336,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)];
}
@ -277,11 +349,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;
}
@ -328,19 +410,151 @@ 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 (sender.state == UIGestureRecognizerStateRecognized) {
TSInteraction *interaction = self.viewItem.interaction;
OWSAssert(interaction);
[self.delegate didTapSystemMessageWithInteraction: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);
return nil;
}
- (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 nil;
case TSInfoMessageTypeGroupQuit:
return nil;
case TSInfoMessageTypeDisappearingMessagesUpdate:
return [SystemMessageAction actionWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS_TAP_TO_CHANGE",
@"Label for button that opens conversation settings.")
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);
return nil;
}
- (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);
@ -353,6 +567,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

@ -20,7 +20,6 @@
#import "NSAttributedString+OWS.h"
#import "NewGroupViewController.h"
#import "OWSAudioPlayer.h"
#import "OWSCallMessageCell.h"
#import "OWSContactOffersCell.h"
#import "OWSConversationSettingsViewController.h"
#import "OWSConversationSettingsViewDelegate.h"
@ -642,8 +641,6 @@ typedef enum : NSUInteger {
{
[self.collectionView registerClass:[OWSSystemMessageCell class]
forCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]];
[self.collectionView registerClass:[OWSCallMessageCell class]
forCellWithReuseIdentifier:[OWSCallMessageCell cellReuseIdentifier]];
[self.collectionView registerClass:[OWSUnreadIndicatorCell class]
forCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier]];
[self.collectionView registerClass:[OWSContactOffersCell class]
@ -1866,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) {
@ -1923,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
@ -2521,32 +2439,6 @@ typedef enum : NSUInteger {
[self.inputToolbar beginEditingTextMessage];
}
#pragma mark - Calls
- (void)didTapCall:(TSCall *)call
{
OWSAssertIsOnMainThread();
OWSAssert([call isKindOfClass:[TSCall class]]);
[self handleCallTap:call];
}
#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 {
OWSFail(@"Tap for system messages of unknown type: %@", [interaction class]);
}
}
#pragma mark - ContactEditingDelegate
- (void)didFinishEditingContact

@ -4,7 +4,6 @@
#import "ConversationViewItem.h"
#import "OWSAudioMessageView.h"
#import "OWSCallMessageCell.h"
#import "OWSContactOffersCell.h"
#import "OWSMessageCell.h"
#import "OWSSystemMessageCell.h"
@ -231,10 +230,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
break;
case OWSInteractionType_Error:
case OWSInteractionType_Info:
measurementCell = [OWSSystemMessageCell new];
break;
case OWSInteractionType_Call:
measurementCell = [OWSCallMessageCell new];
measurementCell = [OWSSystemMessageCell new];
break;
case OWSInteractionType_UnreadIndicator:
measurementCell = [OWSUnreadIndicatorCell new];
@ -298,10 +295,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
forIndexPath:indexPath];
case OWSInteractionType_Error:
case OWSInteractionType_Info:
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
forIndexPath:indexPath];
case OWSInteractionType_Call:
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSCallMessageCell cellReuseIdentifier]
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
forIndexPath:indexPath];
case OWSInteractionType_UnreadIndicator:
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier]

@ -3429,6 +3429,10 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
withCallNumber:@"+19174054215"
callType:RPRecentCallTypeIncomingDeclined
inThread:contactThread]];
[result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
withCallNumber:@"+19174054215"
callType:RPRecentCallTypeOutgoingMissed
inThread:contactThread]];
}
{

@ -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,24 +284,18 @@
/* 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.";
/* Alert title when calling and permissions for microphone are missing */
"CALL_AUDIO_PERMISSION_TITLE" = "Microphone Access Required";
/* Message recorded in conversation history when local user declined a call. */
"CALL_DECLINED" = "Call Declined";
/* Message recorded in conversation history when local user is making or has completed a call. */
"CALL_DEFAULT_STATUS" = "Contact Called";
/* Accessibility label for placing call button */
"CALL_LABEL" = "Call";
/* Message recorded in conversation history when local user missed a call. */
"CALL_MISSED" = "Missed Call";
/* Call setup status label after outgoing call times out */
"CALL_SCREEN_STATUS_NO_ANSWER" = "No Answer.";
@ -557,6 +551,9 @@
/* Label for 'new contact' button in conversation settings view. */
"CONVERSATION_SETTINGS_NEW_CONTACT" = "Create New Contact";
/* Label for button that opens conversation settings. */
"CONVERSATION_SETTINGS_TAP_TO_CHANGE" = "Tap to Change";
/* Label for button to unmute a thread. */
"CONVERSATION_SETTINGS_UNMUTE_ACTION" = "Unmute";
@ -880,7 +877,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.";
@ -898,7 +895,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: %@";
@ -1063,7 +1060,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.";
@ -1434,7 +1431,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" = "%@…";
@ -2112,7 +2112,7 @@
"SHARE_EXTENSION_VIEW_TITLE" = "Share to Signal";
/* Action sheet item */
"SHOW_SAFETY_NUMBER_ACTION" = "Show New Safety Number";
"SHOW_SAFETY_NUMBER_ACTION" = "Show Safety Number";
/* notification action */
"SHOW_THREAD_BUTTON_TITLE" = "Show Conversation";
@ -2129,6 +2129,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";

@ -170,11 +170,6 @@ public class ConversationStyle: NSObject {
}
}
@objc
public func bubbleColor(call: TSCall) -> UIColor {
return bubbleColor(isIncoming: call.isIncoming)
}
@objc
public func bubbleColor(isIncoming: Bool) -> UIColor {
if isIncoming {
@ -200,11 +195,6 @@ public class ConversationStyle: NSObject {
}
}
@objc
public func bubbleTextColor(call: TSCall) -> UIColor {
return bubbleTextColor(isIncoming: call.isIncoming)
}
@objc
public func bubbleTextColor(isIncoming: Bool) -> UIColor {
if isIncoming {

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

@ -17,15 +17,16 @@ typedef enum {
RPRecentCallTypeOutgoingIncomplete,
RPRecentCallTypeIncomingIncomplete,
RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity,
RPRecentCallTypeIncomingDeclined
RPRecentCallTypeIncomingDeclined,
RPRecentCallTypeOutgoingMissed,
} RPRecentCallType;
NSString *NSStringFromCallType(RPRecentCallType callType);
@interface TSCall : TSInteraction <OWSReadTracking, OWSPreviewText>
@property (nonatomic, readonly) RPRecentCallType callType;
@property (nonatomic, readonly) BOOL isIncoming;
- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
@ -36,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,8 +58,11 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
_callSchemaVersion = TSCallCurrentSchemaVersion;
_callType = callType;
if (_callType == RPRecentCallTypeIncomingMissed
|| _callType == RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity) {
// Ensure users are notified of missed calls.
BOOL isIncomingMissed = (_callType == RPRecentCallTypeIncomingMissed
|| _callType == RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity);
if (isIncomingMissed) {
_read = NO;
} else {
_read = YES;
@ -87,6 +112,9 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
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.");
}
}
@ -126,35 +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];
- (BOOL)isIncoming
{
switch (self.callType) {
case RPRecentCallTypeIncoming:
case RPRecentCallTypeIncomingMissed:
case RPRecentCallTypeIncomingIncomplete:
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
case RPRecentCallTypeIncomingDeclined:
return YES;
case RPRecentCallTypeOutgoing:
case RPRecentCallTypeOutgoingIncomplete:
return NO;
}
// 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