diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index d321930cd..179111701 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ 76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; + 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; }; 7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; }; 7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BCD116C27016062006330F1 /* WebRTCSession+DataChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */; }; @@ -1112,6 +1113,7 @@ 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; + 7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = ""; }; 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -2114,6 +2116,7 @@ B8D84EA225DF745A005A043E /* LinkPreviewState.swift */, B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */, 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */, + 7B7CB188270430D20079FF93 /* CallMessageView.swift */, ); path = "Content Views"; sourceTree = ""; @@ -4899,6 +4902,7 @@ 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */, 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, + 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */, C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */, B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */, 3488F9362191CC4000E524CC /* MediaView.swift in Sources */, diff --git a/Session/Conversations/ConversationViewItem.h b/Session/Conversations/ConversationViewItem.h index 8aeaccd01..bbdd12f85 100644 --- a/Session/Conversations/ConversationViewItem.h +++ b/Session/Conversations/ConversationViewItem.h @@ -15,6 +15,7 @@ typedef NS_ENUM(NSInteger, OWSMessageCellType) { OWSMessageCellType_GenericAttachment, OWSMessageCellType_MediaMessage, OWSMessageCellType_OversizeTextDownloading, + OWSMessageCellType_CallMessage, OWSMessageCellType_DeletedMessage }; diff --git a/Session/Conversations/ConversationViewItem.m b/Session/Conversations/ConversationViewItem.m index a60beeab4..5a70120a7 100644 --- a/Session/Conversations/ConversationViewItem.m +++ b/Session/Conversations/ConversationViewItem.m @@ -32,6 +32,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return @"OWSMessageCellType_MediaMessage"; case OWSMessageCellType_OversizeTextDownloading: return @"OWSMessageCellType_OversizeTextDownloading"; + case OWSMessageCellType_CallMessage: + return @"OWSMessageCellType_CallMessage"; + case OWSMessageCellType_DeletedMessage: + return @"OWSMessageCellType_DeletedMessage"; } } @@ -475,6 +479,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.messageCellType = OWSMessageCellType_DeletedMessage; return; } + + if (message.isCallMessage) { + self.messageCellType = OWSMessageCellType_CallMessage; + return; + } // Check for quoted replies _before_ media album handling, // since that logic may exit early. diff --git a/Session/Conversations/Message Cells/Content Views/CallMessageView.swift b/Session/Conversations/Message Cells/Content Views/CallMessageView.swift new file mode 100644 index 000000000..359d1ce98 --- /dev/null +++ b/Session/Conversations/Message Cells/Content Views/CallMessageView.swift @@ -0,0 +1,51 @@ + +final class CallMessageView : UIView { + private let viewItem: ConversationViewItem + private let textColor: UIColor + + // MARK: Settings + private static let iconSize: CGFloat = 24 + private static let iconImageViewSize: CGFloat = 40 + + // MARK: Lifecycle + init(viewItem: ConversationViewItem, textColor: UIColor) { + self.viewItem = viewItem + self.textColor = textColor + super.init(frame: CGRect.zero) + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(viewItem:textColor:) instead.") + } + + private func setUpViewHierarchy() { + guard let message = viewItem.interaction as? TSMessage else { preconditionFailure() } + // Image view + let iconSize = CallMessageView.iconSize + let icon = UIImage(named: "Phone")?.withTint(textColor)?.resizedImage(to: CGSize(width: iconSize, height: iconSize)) + let imageView = UIImageView(image: icon) + imageView.contentMode = .center + let iconImageViewSize = CallMessageView.iconImageViewSize + imageView.set(.width, to: iconImageViewSize) + imageView.set(.height, to: iconImageViewSize) + // Body label + let titleLabel = UILabel() + titleLabel.lineBreakMode = .byTruncatingTail + titleLabel.text = message.body + titleLabel.textColor = textColor + titleLabel.font = .systemFont(ofSize: Values.mediumFontSize) + // Stack view + let stackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ]) + stackView.axis = .horizontal + stackView.alignment = .center + stackView.isLayoutMarginsRelativeArrangement = true + stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 12) + addSubview(stackView) + stackView.pin(to: self, withInset: Values.smallSpacing) + } +} diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 5d66f6401..db758e437 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -275,7 +275,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { timerViewOutgoingMessageConstraint.isActive = (direction == .outgoing) timerViewIncomingMessageConstraint.isActive = (direction == .incoming) // Swipe to reply - if (message.isDeleted) { + if (message.isDeleted || message.isCallMessage) { removeGestureRecognizer(panGestureRecognizer) } else { addGestureRecognizer(panGestureRecognizer) @@ -398,6 +398,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { let deletedMessageView = DeletedMessageView(viewItem: viewItem, textColor: bodyLabelTextColor) snContentView.addSubview(deletedMessageView) deletedMessageView.pin(to: snContentView) + case .callMessage: + let callMessageView = CallMessageView(viewItem: viewItem, textColor: bodyLabelTextColor) + snContentView.addSubview(callMessageView) + callMessageView.pin(to: snContentView) default: return } } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 0b9e70b2a..fb15fc6c9 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -19,7 +19,6 @@ extension AppDelegate { conversationVC.inputAccessoryView?.alpha = 0 presentingVC.present(callVC, animated: true, completion: nil) } else { - } } } diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index c9200293f..6fb19d3a3 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -572,3 +572,5 @@ "OPEN_SETTINGS_BUTTON" = "Settings"; "voice_call" = "Voice Call"; "video_call" = "Video Call"; +"call_outgoing" = "Outgoing Call"; +"call_incoming" = "Incoming Call"; diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index 4e956b725..27996b1b4 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -119,6 +119,8 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { let message = CallMessage() message.kind = .offer message.sdps = [ sdp.sdp ] + let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread) + tsMessage.save(with: transaction) MessageSender.sendNonDurably(message, in: thread, using: transaction).done2 { seal.fulfill(()) }.catch2 { error in diff --git a/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift b/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift index 6849fd50c..b963f68f0 100644 --- a/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift +++ b/SessionMessagingKit/Messages/Signal/TSIncomingMessage+Conversion.swift @@ -1,5 +1,26 @@ public extension TSIncomingMessage { + + static func from(_ callMessage: CallMessage, associatedWith thread: TSThread) -> TSIncomingMessage { + let sender = callMessage.sender! + let result = TSIncomingMessage( + timestamp: callMessage.sentTimestamp!, + in: thread, + authorId: sender, + sourceDeviceId: 1, + messageBody: NSLocalizedString("call_incoming", comment: ""), + attachmentIds: [], + expiresInSeconds: 0, + quotedMessage: nil, + linkPreview: nil, + wasReceivedByUD: true, + openGroupInvitationName: nil, + openGroupInvitationURL: nil, + serverHash: callMessage.serverHash + ) + result.isCallMessage = true + return result + } static func from(_ visibleMessage: VisibleMessage, quotedMessage: TSQuotedMessage?, linkPreview: OWSLinkPreview?, associatedWith thread: TSThread) -> TSIncomingMessage { let sender = visibleMessage.sender! diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.h b/SessionMessagingKit/Messages/Signal/TSMessage.h index ff581bb16..b6096bd43 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.h +++ b/SessionMessagingKit/Messages/Signal/TSMessage.h @@ -40,6 +40,7 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold; @property (nonatomic, readonly, nullable) NSString *openGroupInvitationURL; @property (nonatomic, nullable) NSString *serverHash; @property (nonatomic) BOOL isDeleted; +@property (nonatomic) BOOL isCallMessage; - (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE; diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.m b/SessionMessagingKit/Messages/Signal/TSMessage.m index 59548cae7..39ff5530c 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSMessage.m @@ -87,6 +87,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; _openGroupInvitationURL = openGroupInvitationURL; _serverHash = serverHash; _isDeleted = false; + _isCallMessage = false; return self; } diff --git a/SessionMessagingKit/Messages/Signal/TSOutgoingMessage+Conversion.swift b/SessionMessagingKit/Messages/Signal/TSOutgoingMessage+Conversion.swift index 1509af0f3..83ff29abd 100644 --- a/SessionMessagingKit/Messages/Signal/TSOutgoingMessage+Conversion.swift +++ b/SessionMessagingKit/Messages/Signal/TSOutgoingMessage+Conversion.swift @@ -2,6 +2,13 @@ import SessionUtilitiesKit @objc public extension TSOutgoingMessage { + @objc(fromCallOffer:associatedWith:) + static func from(_ callMessage: CallMessage, associatedWith thread: TSThread) -> TSOutgoingMessage { + let outgoingMessage = TSOutgoingMessage(in: thread, messageBody: NSLocalizedString("call_outgoing", comment: ""), attachmentId: nil) + outgoingMessage.isCallMessage = true + return outgoingMessage + } + @objc(from:associatedWith:) static func from(_ visibleMessage: VisibleMessage, associatedWith thread: TSThread) -> TSOutgoingMessage { return from(visibleMessage, associatedWith: thread, using: nil) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index aa5a8de47..764476bd3 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -280,6 +280,13 @@ extension MessageReceiver { // Delegate to the main app, which is expected to show a dialog confirming // that the user wants to pick up the call. When they do, the SDP contained // in the offer message will be passed to WebRTCSession.handleRemoteSDP(_:from:). + let storage = SNMessagingKitConfiguration.shared.storage + let transaction = transaction as! YapDatabaseReadWriteTransaction + if let threadID = storage.getOrCreateThread(for: message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: nil, using: transaction), + let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) { + let tsMessage = TSIncomingMessage.from(message, associatedWith: thread) + tsMessage.save(with: transaction) + } handleOfferCallMessage?(message) case .answer: print("[Calls] Received answer message.")