diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 06abc7c47..3378c94a0 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 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 */; }; @@ -640,6 +641,8 @@ 341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMisc.m; sourceTree = ""; }; 34277A5C20751BDC006049F2 /* OWSQuotedMessageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSQuotedMessageView.m; sourceTree = ""; }; 34277A5D20751BDC006049F2 /* OWSQuotedMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQuotedMessageView.h; sourceTree = ""; }; + 3427C63E20EFD43D00EEC730 /* OWSCallMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSCallMessageCell.m; sourceTree = ""; }; + 3427C63F20EFD43E00EEC730 /* OWSCallMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSCallMessageCell.h; sourceTree = ""; }; 3430FE171F7751D4000EC51B /* GiphyAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyAPI.swift; sourceTree = ""; }; 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "fontawesome-webfont.ttf"; sourceTree = ""; }; 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dripicons-v2.ttf"; sourceTree = ""; }; @@ -1753,6 +1756,8 @@ 34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */, 34DBF002206BD5A500025978 /* OWSBubbleView.h */, 34DBF001206BD5A500025978 /* OWSBubbleView.m */, + 3427C63F20EFD43E00EEC730 /* OWSCallMessageCell.h */, + 3427C63E20EFD43D00EEC730 /* OWSCallMessageCell.m */, 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */, 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */, 3403B95C20EA9527001A1F44 /* OWSContactShareButtonsView.h */, @@ -3200,6 +3205,7 @@ 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 */, diff --git a/Signal/Images.xcassets/phone-down.imageset/Contents.json b/Signal/Images.xcassets/phone-down.imageset/Contents.json new file mode 100644 index 000000000..9f4a311e4 --- /dev/null +++ b/Signal/Images.xcassets/phone-down.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "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" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/phone-down.imageset/phonedown-20@1x.png b/Signal/Images.xcassets/phone-down.imageset/phonedown-20@1x.png new file mode 100644 index 000000000..fb2b5a1b7 Binary files /dev/null and b/Signal/Images.xcassets/phone-down.imageset/phonedown-20@1x.png differ diff --git a/Signal/Images.xcassets/phone-down.imageset/phonedown-20@2x.png b/Signal/Images.xcassets/phone-down.imageset/phonedown-20@2x.png new file mode 100644 index 000000000..8d69dc0cb Binary files /dev/null and b/Signal/Images.xcassets/phone-down.imageset/phonedown-20@2x.png differ diff --git a/Signal/Images.xcassets/phone-down.imageset/phonedown-20@3x.png b/Signal/Images.xcassets/phone-down.imageset/phonedown-20@3x.png new file mode 100644 index 000000000..badca9731 Binary files /dev/null and b/Signal/Images.xcassets/phone-down.imageset/phonedown-20@3x.png differ diff --git a/Signal/Images.xcassets/phone-up.imageset/Contents.json b/Signal/Images.xcassets/phone-up.imageset/Contents.json new file mode 100644 index 000000000..80c9d1362 --- /dev/null +++ b/Signal/Images.xcassets/phone-up.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "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" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/phone-up.imageset/phoneup-20@1x.png b/Signal/Images.xcassets/phone-up.imageset/phoneup-20@1x.png new file mode 100644 index 000000000..cf8e87311 Binary files /dev/null and b/Signal/Images.xcassets/phone-up.imageset/phoneup-20@1x.png differ diff --git a/Signal/Images.xcassets/phone-up.imageset/phoneup-20@2x.png b/Signal/Images.xcassets/phone-up.imageset/phoneup-20@2x.png new file mode 100644 index 000000000..3419e69be Binary files /dev/null and b/Signal/Images.xcassets/phone-up.imageset/phoneup-20@2x.png differ diff --git a/Signal/Images.xcassets/phone-up.imageset/phoneup-20@3x.png b/Signal/Images.xcassets/phone-up.imageset/phoneup-20@3x.png new file mode 100644 index 000000000..1c9ec9d2b Binary files /dev/null and b/Signal/Images.xcassets/phone-up.imageset/phoneup-20@3x.png differ diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index b092f241e..6e8115bf6 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @class OWSContactsManager; @class TSAttachmentPointer; @class TSAttachmentStream; +@class TSCall; @class TSInteraction; @class TSMessage; @class TSOutgoingMessage; @@ -25,6 +26,10 @@ 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. diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h index fdbc217fa..9936e6c14 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -2,8 +2,6 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -#import "OWSBubbleView.h" - NS_ASSUME_NONNULL_BEGIN extern const CGFloat kOWSMessageCellCornerRadius_Large; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSCallMessageCell.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSCallMessageCell.h new file mode 100644 index 000000000..03a53fda5 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSCallMessageCell.h @@ -0,0 +1,17 @@ +// +// 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 diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSCallMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSCallMessageCell.m new file mode 100644 index 000000000..be2b5d587 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSCallMessageCell.m @@ -0,0 +1,383 @@ +// +// 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 +#import +#import +#import +#import +#import + +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 *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]; + 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 diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m index 014d07120..4be079f3e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m @@ -11,7 +11,6 @@ #import #import #import -#import #import #import @@ -183,8 +182,6 @@ NS_ASSUME_NONNULL_BEGIN } break; } - } else if ([interaction isKindOfClass:[TSCall class]]) { - result = [UIImage imageNamed:@"system_message_call"]; } else { OWSFail(@"Unknown interaction type: %@", [interaction class]); return nil; @@ -234,9 +231,6 @@ 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; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index a4ddb7b0c..b97b1daec 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -20,6 +20,7 @@ #import "NSAttributedString+OWS.h" #import "NewGroupViewController.h" #import "OWSAudioPlayer.h" +#import "OWSCallMessageCell.h" #import "OWSContactOffersCell.h" #import "OWSConversationSettingsViewController.h" #import "OWSConversationSettingsViewDelegate.h" @@ -606,6 +607,8 @@ 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] @@ -2483,6 +2486,16 @@ 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 @@ -2494,8 +2507,6 @@ typedef enum : NSUInteger { [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]); } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 810a4ea49..1bbc5c5d1 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -4,6 +4,7 @@ #import "ConversationViewItem.h" #import "OWSAudioMessageView.h" +#import "OWSCallMessageCell.h" #import "OWSContactOffersCell.h" #import "OWSMessageCell.h" #import "OWSSystemMessageCell.h" @@ -230,9 +231,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) break; case OWSInteractionType_Error: case OWSInteractionType_Info: - case OWSInteractionType_Call: measurementCell = [OWSSystemMessageCell new]; break; + case OWSInteractionType_Call: + measurementCell = [OWSCallMessageCell new]; + break; case OWSInteractionType_UnreadIndicator: measurementCell = [OWSUnreadIndicatorCell new]; break; @@ -292,9 +295,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) forIndexPath:indexPath]; case OWSInteractionType_Error: case OWSInteractionType_Info: - case OWSInteractionType_Call: return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier] forIndexPath:indexPath]; + case OWSInteractionType_Call: + return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSCallMessageCell cellReuseIdentifier] + forIndexPath:indexPath]; case OWSInteractionType_UnreadIndicator: return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier] forIndexPath:indexPath]; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index b62ccfc4a..dbea7315f 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -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:RPRecentCallTypeIncomingDeclined + inThread:contactThread]]; } { diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 481a2db15..a14218903 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -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 diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 1e6b7624e..055a0c775 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -290,9 +290,18 @@ /* 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."; diff --git a/SignalMessaging/utils/ConversationStyle.swift b/SignalMessaging/utils/ConversationStyle.swift index 03d69ba32..25fc91c9e 100644 --- a/SignalMessaging/utils/ConversationStyle.swift +++ b/SignalMessaging/utils/ConversationStyle.swift @@ -169,6 +169,15 @@ public class ConversationStyle: NSObject { } } + @objc + public func bubbleColor(call: TSCall) -> UIColor { + if call.isIncoming { + return primaryColor + } else { + return self.bubbleColorOutgoingSent + } + } + @objc public static var bubbleTextColorIncoming = UIColor.ows_white @@ -190,4 +199,13 @@ public class ConversationStyle: NSObject { return UIColor.ows_materialBlue } } + + @objc + public func bubbleTextColor(call: TSCall) -> UIColor { + if call.isIncoming { + return ConversationStyle.bubbleTextColorIncoming + } else { + return UIColor.ows_black + } + } } diff --git a/SignalServiceKit/src/Messages/TSCall.h b/SignalServiceKit/src/Messages/TSCall.h index fdca7d4d1..76915e7e9 100644 --- a/SignalServiceKit/src/Messages/TSCall.h +++ b/SignalServiceKit/src/Messages/TSCall.h @@ -12,11 +12,11 @@ NS_ASSUME_NONNULL_BEGIN typedef enum { RPRecentCallTypeIncoming = 1, RPRecentCallTypeOutgoing, - RPRecentCallTypeMissed, + RPRecentCallTypeIncomingMissed, // These call types are used until the call connects. RPRecentCallTypeOutgoingIncomplete, RPRecentCallTypeIncomingIncomplete, - RPRecentCallTypeMissedBecauseOfChangedIdentity, + RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity, RPRecentCallTypeIncomingDeclined } RPRecentCallType; @@ -24,6 +24,8 @@ typedef enum { @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 diff --git a/SignalServiceKit/src/Messages/TSCall.m b/SignalServiceKit/src/Messages/TSCall.m index 20d805c09..db45d0c56 100644 --- a/SignalServiceKit/src/Messages/TSCall.m +++ b/SignalServiceKit/src/Messages/TSCall.m @@ -36,7 +36,8 @@ NSUInteger TSCallCurrentSchemaVersion = 1; _callSchemaVersion = TSCallCurrentSchemaVersion; _callType = callType; - if (_callType == RPRecentCallTypeMissed || _callType == RPRecentCallTypeMissedBecauseOfChangedIdentity) { + if (_callType == RPRecentCallTypeIncomingMissed + || _callType == RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity) { _read = NO; } else { _read = YES; @@ -75,13 +76,13 @@ 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", @@ -141,6 +142,21 @@ NSUInteger TSCallCurrentSchemaVersion = 1; }]; } +- (BOOL)isIncoming +{ + switch (self.callType) { + case RPRecentCallTypeIncoming: + case RPRecentCallTypeIncomingMissed: + case RPRecentCallTypeIncomingIncomplete: + case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity: + case RPRecentCallTypeIncomingDeclined: + return YES; + case RPRecentCallTypeOutgoing: + case RPRecentCallTypeOutgoingIncomplete: + return NO; + } +} + @end NS_ASSUME_NONNULL_END