diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index cd2c46514..97c79506e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -137,6 +137,7 @@ 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; 76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; }; 7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */; }; + 7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */; }; 7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E1271E743B00848B49 /* OWSSounds.swift */; }; 7B1581E4271FC59D00848B49 /* CallModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E3271FC59C00848B49 /* CallModal.swift */; }; 7B1581E6271FD2A100848B49 /* VideoPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E5271FD2A100848B49 /* VideoPreviewVC.swift */; }; @@ -1127,6 +1128,7 @@ 76EB03C218170B33006006FC /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampUtils.swift; sourceTree = ""; }; + 7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageCell.swift; sourceTree = ""; }; 7B1581E1271E743B00848B49 /* OWSSounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSSounds.swift; sourceTree = ""; }; 7B1581E3271FC59C00848B49 /* CallModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallModal.swift; sourceTree = ""; }; 7B1581E5271FD2A100848B49 /* VideoPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPreviewVC.swift; sourceTree = ""; }; @@ -2245,6 +2247,7 @@ B835247825C38D880089A44F /* MessageCell.swift */, B835249A25C3AB650089A44F /* VisibleMessageCell.swift */, B83524A425C3BA4B0089A44F /* InfoMessageCell.swift */, + 7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */, B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */, B8041A7325C8F758003C2166 /* Content Views */, ); @@ -5003,6 +5006,7 @@ B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */, 76EB054018170B33006006FC /* AppDelegate.m in Sources */, 340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */, + 7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */, C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */, C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */, B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */, diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 990833cfb..ed3ccc1be 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -133,7 +133,7 @@ public final class SessionCallManager: NSObject { callUpdate.supportsDTMF = false } - public func handleIncomingCallOfferInBusyOrUnenabledState(offerMessage: CallMessage, using transaction: YapDatabaseReadWriteTransaction) { + public func handleIncomingCallOfferInBusyState(offerMessage: CallMessage, using transaction: YapDatabaseReadWriteTransaction) { guard let caller = offerMessage.sender, let thread = TSContactThread.fetch(for: caller, using: transaction) else { return } let message = CallMessage() message.uuid = offerMessage.uuid diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift new file mode 100644 index 000000000..1715210a7 --- /dev/null +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -0,0 +1,70 @@ +import UIKit + +final class CallMessageCell : MessageCell { + private lazy var iconImageViewWidthConstraint = iconImageView.set(.width, to: CallMessageCell.iconSize) + private lazy var iconImageViewHeightConstraint = iconImageView.set(.height, to: CallMessageCell.iconSize) + + // MARK: UI Components + private lazy var iconImageView = UIImageView() + + private lazy var label: UILabel = { + let result = UILabel() + result.numberOfLines = 0 + result.lineBreakMode = .byWordWrapping + result.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.textColor = Colors.text + result.textAlignment = .center + return result + }() + + private lazy var container: UIView = { + let result = UIView() + result.set(.height, to: 50) + result.layer.cornerRadius = 18 + result.backgroundColor = Colors.callMessageBackground + result.addSubview(label) + label.autoCenterInSuperview() + result.addSubview(iconImageView) + iconImageView.autoVCenterInSuperview() + iconImageView.pin(.left, to: .left, of: result, withInset: CallMessageCell.inset) + return result + }() + + // MARK: Settings + private static let iconSize: CGFloat = 16 + private static let inset = Values.mediumSpacing + private static let margin = UIScreen.main.bounds.width * 0.1 + + override class var identifier: String { "CallMessageCell" } + + // MARK: Lifecycle + override func setUpViewHierarchy() { + super.setUpViewHierarchy() + iconImageViewWidthConstraint.isActive = true + iconImageViewHeightConstraint.isActive = true + addSubview(container) + container.pin(.left, to: .left, of: self, withInset: CallMessageCell.margin) + container.pin(.top, to: .top, of: self, withInset: CallMessageCell.inset) + container.pin(.right, to: .right, of: self, withInset: -CallMessageCell.margin) + container.pin(.bottom, to: .bottom, of: self, withInset: -CallMessageCell.inset) + } + + // MARK: Updating + override func update() { + guard let message = viewItem?.interaction as? TSMessage, message.isCallMessage else { return } + let icon: UIImage? + switch message.interactionType() { + case .outgoingMessage: icon = UIImage(named: "CallOutgoing") + case .incomingMessage: icon = UIImage(named: "CallIncoming") + default: icon = nil + } + if let icon = icon { + iconImageView.image = icon.withTint(Colors.text) + } + iconImageViewWidthConstraint.constant = (icon != nil) ? CallMessageCell.iconSize : 0 + iconImageViewHeightConstraint.constant = (icon != nil) ? CallMessageCell.iconSize : 0 + Storage.read { transaction in + self.label.text = message.previewText(with: transaction) + } + } +} diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index a2d846692..56d93b2b2 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -46,7 +46,11 @@ class MessageCell : UITableViewCell { static func getCellType(for viewItem: ConversationViewItem) -> MessageCell.Type { switch viewItem.interaction { case is TSIncomingMessage: fallthrough - case is TSOutgoingMessage: return VisibleMessageCell.self + case is TSOutgoingMessage: + if let message = viewItem.interaction as? TSMessage, message.isCallMessage { + return CallMessageCell.self + } + return VisibleMessageCell.self case is TSInfoMessage: return InfoMessageCell.self case is TypingIndicatorInteraction: return TypingIndicatorCell.self default: preconditionFailure() diff --git a/Session/Conversations/Views & Modals/MessagesTableView.swift b/Session/Conversations/Views & Modals/MessagesTableView.swift index d5648f2bb..0f19cff94 100644 --- a/Session/Conversations/Views & Modals/MessagesTableView.swift +++ b/Session/Conversations/Views & Modals/MessagesTableView.swift @@ -31,6 +31,7 @@ final class MessagesTableView : UITableView { register(VisibleMessageCell.self, forCellReuseIdentifier: VisibleMessageCell.identifier) register(InfoMessageCell.self, forCellReuseIdentifier: InfoMessageCell.identifier) register(TypingIndicatorCell.self, forCellReuseIdentifier: TypingIndicatorCell.identifier) + register(CallMessageCell.self, forCellReuseIdentifier: CallMessageCell.identifier) separatorStyle = .none backgroundColor = .clear showsVerticalScrollIndicator = false diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 0d2bbdeb9..7ca15247e 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -51,11 +51,15 @@ extension AppDelegate { // Pre offer messages MessageReceiver.handleNewCallOfferMessageIfNeeded = { (message, transaction) in guard CurrentAppContext().isMainApp else { return } + guard SSKPreferences.areCallsEnabled else { + // TODO: Show tips and insert a missing call message + return + } let callManager = AppEnvironment.shared.callManager // Ignore pre offer message after the same call instance has been generated if let currentCall = callManager.currentCall, currentCall.uuid == message.uuid! { return } - guard callManager.currentCall == nil && SSKPreferences.areCallsEnabled else { - callManager.handleIncomingCallOfferInBusyOrUnenabledState(offerMessage: message, using: transaction) + guard callManager.currentCall == nil else { + callManager.handleIncomingCallOfferInBusyState(offerMessage: message, using: transaction) return } // Create incoming call message diff --git a/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/CallIncoming.pdf b/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/CallIncoming.pdf new file mode 100644 index 000000000..fbc5a4a87 Binary files /dev/null and b/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/CallIncoming.pdf differ diff --git a/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/Contents.json b/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/Contents.json new file mode 100644 index 000000000..083076594 --- /dev/null +++ b/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "CallIncoming.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Session/Meta/Images.xcassets/Session/CallOutgoing.imageset/CallOutgoing.pdf b/Session/Meta/Images.xcassets/Session/CallOutgoing.imageset/CallOutgoing.pdf new file mode 100644 index 000000000..f6550ced9 Binary files /dev/null and b/Session/Meta/Images.xcassets/Session/CallOutgoing.imageset/CallOutgoing.pdf differ diff --git a/Session/Meta/Images.xcassets/Session/CallOutgoing.imageset/Contents.json b/Session/Meta/Images.xcassets/Session/CallOutgoing.imageset/Contents.json new file mode 100644 index 000000000..d6bb5d534 --- /dev/null +++ b/Session/Meta/Images.xcassets/Session/CallOutgoing.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "CallOutgoing.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Session/Shared/ConversationCell.swift b/Session/Shared/ConversationCell.swift index 2eaf22d3a..d0e98b494 100644 --- a/Session/Shared/ConversationCell.swift +++ b/Session/Shared/ConversationCell.swift @@ -228,7 +228,7 @@ final class ConversationCell : UITableViewCell { } statusIndicatorView.backgroundColor = nil let lastMessage = threadViewModel.lastMessageForInbox - if let lastMessage = lastMessage as? TSOutgoingMessage { + if let lastMessage = lastMessage as? TSOutgoingMessage, !lastMessage.isCallMessage { let image: UIImage let status = MessageRecipientStatusUtils.recipientStatus(outgoingMessage: lastMessage) switch status { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 8c4ecba30..184111889 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -269,10 +269,12 @@ extension MessageReceiver { public static func handleCallMessage(_ message: CallMessage, using transaction: Any) { guard let timestamp = message.sentTimestamp, TimestampUtils.isWithinOneMinute(timestamp: timestamp) else { return } + let transaction = transaction as! YapDatabaseReadWriteTransaction + // Ignore call messages from threads without outgoing messages + guard let sender = message.sender, let thread = TSContactThread.fetch(for: sender, using: transaction), thread.hasOutgoingInteraction(with: transaction) else { return } switch message.kind! { case .preOffer: print("[Calls] Received pre-offer message.") - let transaction = transaction as! YapDatabaseReadWriteTransaction handleNewCallOfferMessageIfNeeded?(message, transaction) case .offer: print("[Calls] Received offer message.") diff --git a/SessionMessagingKit/Threads/TSThread.h b/SessionMessagingKit/Threads/TSThread.h index c2449397a..71818c3d0 100644 --- a/SessionMessagingKit/Threads/TSThread.h +++ b/SessionMessagingKit/Threads/TSThread.h @@ -38,6 +38,13 @@ BOOL IsNoteToSelfEnabled(void); */ - (NSString *)name; +/** + * Returns if there is any outgoing interations in this thread. + * + * @return YES if there are outgoing interations, NO otherwise. + */ +- (BOOL)hasOutgoingInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction; + /** * @returns recipientId for each recipient in the thread */ diff --git a/SessionMessagingKit/Threads/TSThread.m b/SessionMessagingKit/Threads/TSThread.m index 6283a324d..bf4bad523 100644 --- a/SessionMessagingKit/Threads/TSThread.m +++ b/SessionMessagingKit/Threads/TSThread.m @@ -153,6 +153,18 @@ BOOL IsNoteToSelfEnabled(void) return @[]; } +- (BOOL)hasOutgoingInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + __block BOOL hasOutgoingInteraction = NO; + [self enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, BOOL *stop) { + if ([interaction interactionType] == OWSInteractionType_OutgoingMessage) { + hasOutgoingInteraction = YES; + *stop = YES; + } + }]; + return hasOutgoingInteraction; +} + #pragma mark Interactions /** diff --git a/SessionUIKit/Style Guide/Colors.swift b/SessionUIKit/Style Guide/Colors.swift index 06f4e3095..e6e092829 100644 --- a/SessionUIKit/Style Guide/Colors.swift +++ b/SessionUIKit/Style Guide/Colors.swift @@ -41,4 +41,5 @@ public final class Colors : NSObject { @objc public static var pnOptionBackground: UIColor { UIColor(named: "session_pn_option_background")! } @objc public static var pnOptionBorder: UIColor { UIColor(named: "session_pn_option_border")! } @objc public static var pathsBuilding: UIColor { UIColor(named: "session_paths_building")! } + @objc public static var callMessageBackground: UIColor { UIColor(named: "session_call_message_background")! } } diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_call_message_background.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_call_message_background.colorset/Contents.json new file mode 100644 index 000000000..843f0f6c1 --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_call_message_background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF5", + "green" : "0xF5", + "red" : "0xF5" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x21", + "green" : "0x21", + "red" : "0x21" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +}