diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 97c79506e..569aadfef 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 */; }; 7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */; }; 7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */; }; + 7B0EFDF2275449AA00FFAAE7 /* TSInfoMessage+Calls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDF1275449AA00FFAAE7 /* TSInfoMessage+Calls.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 */; }; @@ -1129,6 +1130,7 @@ 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 = ""; }; + 7B0EFDF1275449AA00FFAAE7 /* TSInfoMessage+Calls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+Calls.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 = ""; }; @@ -2633,6 +2635,7 @@ C33FDB56255A580D00E217F9 /* TSOutgoingMessage.m */, B84072952565E9F50037CB17 /* TSOutgoingMessage+Conversion.swift */, 34B6A904218B4C90007C4606 /* TypingIndicatorInteraction.swift */, + 7B0EFDF1275449AA00FFAAE7 /* TSInfoMessage+Calls.swift */, ); path = Signal; sourceTree = ""; @@ -4847,6 +4850,7 @@ C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */, C32C59C7256DB41F003C73A2 /* TSThread.m in Sources */, C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */, + 7B0EFDF2275449AA00FFAAE7 /* TSInfoMessage+Calls.swift in Sources */, C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */, C32C5AF8256DC051003C73A2 /* OWSDisappearingMessagesConfiguration.m in Sources */, C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */, diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 63c94149f..15bc4b4c9 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -14,7 +14,7 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { let webRTCSession: WebRTCSession let isOutgoing: Bool var remoteSDP: RTCSessionDescription? = nil - var callMessageTimestamp: UInt64? + var callMessageID: String? var answerCallAction: CXAnswerCallAction? = nil var contactName: String { let contact = Storage.shared.getContact(with: self.sessionID) @@ -182,12 +182,12 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { // MARK: Actions func startSessionCall() { guard case .offer = mode else { return } - var promise: Promise! + var promise: Promise! Storage.write(with: { transaction in promise = self.webRTCSession.sendPreOffer(to: self.sessionID, using: transaction) }, completion: { [weak self] in - let _ = promise.done { timestamp in - self?.callMessageTimestamp = timestamp + let _ = promise.done { messageID in + self?.callMessageID = messageID Storage.shared.write { transaction in self?.webRTCSession.sendOffer(to: self!.sessionID, using: transaction as! YapDatabaseReadWriteTransaction).retainUntilComplete() } @@ -219,41 +219,28 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { // MARK: Update call message func updateCallMessage(mode: EndCallMode) { - guard let callMessageTimestamp = callMessageTimestamp else { return } + guard let callMessageID = callMessageID else { return } Storage.write { transaction in - let tsMessage: TSMessage? - if self.isOutgoing { - tsMessage = TSOutgoingMessage.find(withTimestamp: callMessageTimestamp) - } else { - tsMessage = TSIncomingMessage.find(withAuthorId: self.sessionID, timestamp: callMessageTimestamp, transaction: transaction) - } - if let messageToUpdate = tsMessage { + let infoMessage = TSInfoMessage.fetch(uniqueId: callMessageID, transaction: transaction) + if let messageToUpdate = infoMessage { var shouldMarkAsRead = false - let newMessageBody: String if self.duration > 0 { - let durationString = NSString.formatDurationSeconds(UInt32(self.duration), useShortFormat: true) - newMessageBody = "\(self.isOutgoing ? NSLocalizedString("call_outgoing", comment: "") : NSLocalizedString("call_incoming", comment: "")): \(durationString)" shouldMarkAsRead = true } else if self.hasStartedConnecting { - newMessageBody = NSLocalizedString("call_cancelled", comment: "") shouldMarkAsRead = true } else { switch mode { - case .local: - newMessageBody = self.isOutgoing ? NSLocalizedString("call_cancelled", comment: "") : NSLocalizedString("call_rejected", comment: "") - shouldMarkAsRead = true - case .remote: - newMessageBody = self.isOutgoing ? NSLocalizedString("call_rejected", comment: "") : NSLocalizedString("call_missing", comment: "") - case .unanswered: - newMessageBody = NSLocalizedString("call_timeout", comment: "") - case .answeredElsewhere: - newMessageBody = messageToUpdate.body! - shouldMarkAsRead = true + case .local: shouldMarkAsRead = true + case .remote: break + case .unanswered: break + case .answeredElsewhere: shouldMarkAsRead = true + } + if messageToUpdate.callState == .incoming { + messageToUpdate.updateCallInfoMessage(.missed, using: transaction) } } - messageToUpdate.updateCall(withNewBody: newMessageBody, transaction: transaction) - if let incomingMessage = tsMessage as? TSIncomingMessage, shouldMarkAsRead { - incomingMessage.markAsReadNow(withSendReadReceipt: false, transaction: transaction) + if shouldMarkAsRead { + messageToUpdate.markAsRead(atTimestamp: NSDate.ows_millisecondTimeStamp(), sendReadReceipt: false, transaction: transaction) } } } diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index ed3ccc1be..a229f92cb 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -140,9 +140,9 @@ public final class SessionCallManager: NSObject { message.kind = .endCall print("[Calls] Sending end call message.") MessageSender.sendNonDurably(message, in: thread, using: transaction).retainUntilComplete() - if let tsMessage = TSIncomingMessage.find(withAuthorId: caller, timestamp: offerMessage.sentTimestamp!, transaction: transaction) { - tsMessage.updateCall(withNewBody: NSLocalizedString("call_missing", comment: ""), transaction: transaction) - } + let infoMessage = TSInfoMessage.from(offerMessage, associatedWith: thread) + infoMessage.save(with: transaction) + infoMessage.updateCallInfoMessage(.missed, using: transaction) } public func invalidateTimeoutTimer() { diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 1715210a7..1c7454967 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -51,11 +51,12 @@ final class CallMessageCell : MessageCell { // MARK: Updating override func update() { - guard let message = viewItem?.interaction as? TSMessage, message.isCallMessage else { return } + guard let message = viewItem?.interaction as? TSInfoMessage, message.messageType == .call else { return } let icon: UIImage? - switch message.interactionType() { - case .outgoingMessage: icon = UIImage(named: "CallOutgoing") - case .incomingMessage: icon = UIImage(named: "CallIncoming") + switch message.callState { + case .outgoing: icon = UIImage(named: "CallOutgoing")?.withTint(Colors.text) + case .incoming: icon = UIImage(named: "CallIncoming")?.withTint(Colors.text) + case .missed: icon = UIImage(named: "CallMissed")?.withTint(Colors.destructive) default: icon = nil } if let icon = icon { @@ -63,8 +64,6 @@ final class CallMessageCell : MessageCell { } 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) - } + self.label.text = message.customMessage } } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 56d93b2b2..e98f0281c 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -46,12 +46,12 @@ class MessageCell : UITableViewCell { static func getCellType(for viewItem: ConversationViewItem) -> MessageCell.Type { switch viewItem.interaction { case is TSIncomingMessage: fallthrough - case is TSOutgoingMessage: - if let message = viewItem.interaction as? TSMessage, message.isCallMessage { + case is TSOutgoingMessage: return VisibleMessageCell.self + case is TSInfoMessage: + if let message = viewItem.interaction as? TSInfoMessage, message.messageType == .call { return CallMessageCell.self } - return VisibleMessageCell.self - case is TSInfoMessage: return InfoMessageCell.self + return InfoMessageCell.self case is TypingIndicatorInteraction: return TypingIndicatorCell.self default: preconditionFailure() } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 7ca15247e..f90ec017c 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -2,6 +2,7 @@ import PromiseKit import WebRTC import SessionUIKit import UIKit +import SessionMessagingKit extension AppDelegate { @@ -64,12 +65,12 @@ extension AppDelegate { } // Create incoming call message let thread = TSContactThread.getOrCreateThread(withContactSessionID: message.sender!, transaction: transaction) - let tsMessage = TSIncomingMessage.from(message, associatedWith: thread) - tsMessage.save(with: transaction) + let infoMessage = TSInfoMessage.from(message, associatedWith: thread) + infoMessage.save(with: transaction) // Handle UI if let caller = message.sender, let uuid = message.uuid { let call = SessionCall(for: caller, uuid: uuid, mode: .answer) - call.callMessageTimestamp = message.sentTimestamp + call.callMessageID = infoMessage.uniqueId self.showCallUIForCall(call) } } diff --git a/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/CallIncoming.pdf b/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/CallIncoming.pdf index fbc5a4a87..93e911237 100644 Binary files a/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/CallIncoming.pdf and b/Session/Meta/Images.xcassets/Session/CallIncoming.imageset/CallIncoming.pdf differ diff --git a/Session/Meta/Images.xcassets/Session/CallMissed.imageset/CallMissed.pdf b/Session/Meta/Images.xcassets/Session/CallMissed.imageset/CallMissed.pdf new file mode 100644 index 000000000..8c6206764 Binary files /dev/null and b/Session/Meta/Images.xcassets/Session/CallMissed.imageset/CallMissed.pdf differ diff --git a/Session/Meta/Images.xcassets/Session/CallMissed.imageset/Contents.json b/Session/Meta/Images.xcassets/Session/CallMissed.imageset/Contents.json new file mode 100644 index 000000000..0aa98d115 --- /dev/null +++ b/Session/Meta/Images.xcassets/Session/CallMissed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "CallMissed.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 index f6550ced9..92206098f 100644 Binary files a/Session/Meta/Images.xcassets/Session/CallOutgoing.imageset/CallOutgoing.pdf and b/Session/Meta/Images.xcassets/Session/CallOutgoing.imageset/CallOutgoing.pdf differ diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 15cf77447..5af272ffc 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -581,9 +581,9 @@ "DISMISS_BUTTON_TEXT" = "Dismiss"; /* Button text which opens the settings app */ "OPEN_SETTINGS_BUTTON" = "Settings"; -"call_outgoing" = "Outgoing Call"; -"call_incoming" = "Incoming Call"; -"call_missing" = "Missing Call"; +"call_outgoing" = "You called %@"; +"call_incoming" = "%@ called you"; +"call_missed" = "Missed Call from %@"; "call_rejected" = "Rejected Call"; "call_cancelled" = "Cancelled Call"; "call_timeout" = "Unanswered Call"; diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index b2299e23c..3344d05ae 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -242,7 +242,12 @@ public enum PushRegistrationError: Error { let payload = payload.dictionaryPayload if let uuid = payload["uuid"] as? String, let caller = payload["caller"] as? String, let timestamp = payload["timestamp"] as? UInt64 { let call = SessionCall(for: caller, uuid: uuid, mode: .answer) - call.callMessageTimestamp = timestamp + Storage.write{ transaction in + let thread = TSContactThread.getOrCreateThread(withContactSessionID: caller, transaction: transaction) + let infoMessage = TSInfoMessage.callInfoMessage(from: caller, timestamp: timestamp, in: thread) + infoMessage.save(with: transaction) + call.callMessageID = infoMessage.uniqueId + } let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.startPollerIfNeeded() appDelegate.startClosedGroupPoller() diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index 65a6246eb..17fa4ec19 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -117,19 +117,21 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { } // MARK: Signaling - public func sendPreOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { + public func sendPreOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { print("[Calls] Sending pre-offer message.") guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) } - let (promise, seal) = Promise.pending() + let (promise, seal) = Promise.pending() DispatchQueue.main.async { let message = CallMessage() + message.sender = getUserHexEncodedPublicKey() + message.sentTimestamp = NSDate.millisecondTimestamp() message.uuid = self.uuid message.kind = .preOffer - let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread) - tsMessage.save(with: transaction) + let infoMessage = TSInfoMessage.from(message, associatedWith: thread) + infoMessage.save(with: transaction) MessageSender.sendNonDurably(message, in: thread, using: transaction).done2 { print("[Calls] Pre-offer message has been sent.") - seal.fulfill((tsMessage.timestamp)) + seal.fulfill((infoMessage.uniqueId)) }.catch2 { error in seal.reject(error) } diff --git a/SessionMessagingKit/Messages/Signal/TSInfoMessage+Calls.swift b/SessionMessagingKit/Messages/Signal/TSInfoMessage+Calls.swift new file mode 100644 index 000000000..fcf0af1be --- /dev/null +++ b/SessionMessagingKit/Messages/Signal/TSInfoMessage+Calls.swift @@ -0,0 +1,40 @@ + +@objc public extension TSInfoMessage { + @objc(fromCallOffer:associatedWith:) + static func from(_ callMessage: CallMessage, associatedWith thread: TSThread) -> TSInfoMessage { + return callInfoMessage(from: callMessage.sender!, timestamp: callMessage.sentTimestamp!, in: thread) + } + + static func callInfoMessage(from caller: String, timestamp: UInt64, in thread: TSThread) -> TSInfoMessage { + let callState: TSInfoMessageCallState + let messageBody: String + var contactName: String = "" + if let contactThread = thread as? TSContactThread { + let sessionID = contactThread.contactSessionID() + contactName = Storage.shared.getContact(with: sessionID)?.displayName(for: Contact.Context.regular) ?? sessionID + } + if caller == getUserHexEncodedPublicKey() { + callState = .outgoing + messageBody = String(format: NSLocalizedString("call_outgoing", comment: ""), contactName) + } else { + callState = .incoming + messageBody = String(format: NSLocalizedString("call_incoming", comment: ""), contactName) + } + let infoMessage = TSInfoMessage.init(timestamp: timestamp, in: thread, messageType: .call, customMessage: messageBody) + infoMessage.callState = callState + return infoMessage + } + + @objc(updateCallInfoMessageWithNewState:usingTransaction:) + func updateCallInfoMessage(_ newCallState: TSInfoMessageCallState, using transaction: YapDatabaseReadWriteTransaction) { + guard self.messageType == .call else { return } + self.callState = newCallState + var contactName: String = "" + if let contactThread = self.thread as? TSContactThread { + let sessionID = contactThread.contactSessionID() + contactName = Storage.shared.getContact(with: sessionID)?.displayName(for: Contact.Context.regular) ?? sessionID + } + self.customMessage = String(format: NSLocalizedString("call_missed", comment: ""), contactName) + self.save(with: transaction) + } +} diff --git a/SessionMessagingKit/Messages/Signal/TSInfoMessage.h b/SessionMessagingKit/Messages/Signal/TSInfoMessage.h index 745ffd0d6..30f607378 100644 --- a/SessionMessagingKit/Messages/Signal/TSInfoMessage.h +++ b/SessionMessagingKit/Messages/Signal/TSInfoMessage.h @@ -15,12 +15,21 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) { TSInfoMessageTypeGroupCurrentUserLeft, TSInfoMessageTypeDisappearingMessagesUpdate, TSInfoMessageTypeScreenshotNotification, - TSInfoMessageTypeMediaSavedNotification + TSInfoMessageTypeMediaSavedNotification, + TSInfoMessageTypeCall +}; + +typedef NS_ENUM(NSInteger, TSInfoMessageCallState) { + TSInfoMessageCallStateIncoming, + TSInfoMessageCallStateOutgoing, + TSInfoMessageCallStateMissed, + TSInfoMessageCallStateUnknown }; @property (atomic, readonly) TSInfoMessageType messageType; -@property (atomic, readonly, nullable) NSString *customMessage; +@property (atomic, nullable) NSString *customMessage; @property (atomic, readonly, nullable) NSString *unregisteredRecipientId; +@property (atomic) TSInfoMessageCallState callState; - (instancetype)initMessageWithTimestamp:(uint64_t)timestamp inThread:(nullable TSThread *)thread diff --git a/SessionMessagingKit/Messages/Signal/TSInfoMessage.m b/SessionMessagingKit/Messages/Signal/TSInfoMessage.m index 67bb6b339..833c1cd37 100644 --- a/SessionMessagingKit/Messages/Signal/TSInfoMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSInfoMessage.m @@ -65,6 +65,7 @@ NSUInteger TSInfoMessageSchemaVersion = 1; } _messageType = infoMessage; + _callState = TSInfoMessageCallStateUnknown; _infoMessageSchemaVersion = TSInfoMessageSchemaVersion; if (self.isDynamicInteraction) {