diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 7a4f678da..95be399f6 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -130,6 +130,7 @@ 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; + 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; }; @@ -158,6 +159,7 @@ 7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; }; 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; }; 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; }; + 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */; }; 7BAADFCC27B0EF23007BCF92 /* CallVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAADFCB27B0EF23007BCF92 /* CallVideoView.swift */; }; 7BAADFCE27B215FE007BCF92 /* UIView+Draggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */; }; 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */; }; @@ -169,6 +171,7 @@ 7BB92B3F28C825FD0082762F /* NewConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB92B3E28C825FD0082762F /* NewConversationViewModel.swift */; }; 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */; }; 7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */; }; + 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC452875600700747E59 /* DocumentTitleViewController.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, ); }; }; 7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; }; @@ -1175,6 +1178,7 @@ 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; 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 = ""; }; + 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = ""; }; @@ -1203,6 +1207,7 @@ 7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = ""; }; 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = ""; }; 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = ""; }; + 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _009_OpenGroupPermission.swift; sourceTree = ""; }; 7BAADFCB27B0EF23007BCF92 /* CallVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVideoView.swift; sourceTree = ""; }; 7BAADFCD27B215FE007BCF92 /* UIView+Draggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Draggable.swift"; sourceTree = ""; }; 7BAF54CC27ACCEEC003D12F8 /* GlobalSearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalSearchViewController.swift; sourceTree = ""; }; @@ -1214,6 +1219,7 @@ 7BB92B3E28C825FD0082762F /* NewConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationViewModel.swift; sourceTree = ""; }; 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.swift; sourceTree = ""; }; 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.swift; sourceTree = ""; }; + 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.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 = ""; }; @@ -2083,6 +2089,7 @@ 34074FC5203E5435004596AE /* messageReceivedSounds */ = { isa = PBXGroup; children = ( + 7B50D64C28AC7CF80086CCEC /* silence.aiff */, 45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */, 45B74A6F2044AAB500CD42F8 /* aurora.aifc */, 45B74A5F2044AAB400CD42F8 /* bamboo-quiet.aifc */, @@ -3555,6 +3562,7 @@ FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */, FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */, + 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */, ); path = Migrations; sourceTree = ""; @@ -4571,6 +4579,7 @@ 45B74A872044AAB600CD42F8 /* complete-quiet.aifc in Resources */, 45B74A772044AAB600CD42F8 /* hello.aifc in Resources */, 45B74A7C2044AAB600CD42F8 /* hello-quiet.aifc in Resources */, + 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */, 45B74A792044AAB600CD42F8 /* input.aifc in Resources */, C3CA3ABE255CDB0D00F4C6D4 /* portuguese.txt in Resources */, 45B74A8C2044AAB600CD42F8 /* input-quiet.aifc in Resources */, @@ -5313,6 +5322,7 @@ FD09799527FE7B8E00936362 /* Interaction.swift in Sources */, FD5C72FF284F0F120029977D /* MessageReceiver+ConfigurationMessages.swift in Sources */, FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */, + 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */, FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, @@ -6955,7 +6965,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 374; + CURRENT_PROJECT_VERSION = 375; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6994,7 +7004,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.1.0; + MARKETING_VERSION = 2.1.1; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7027,7 +7037,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 374; + CURRENT_PROJECT_VERSION = 375; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7066,7 +7076,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.1.0; + MARKETING_VERSION = 2.1.1; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 1aef15a69..1fc8620a5 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -71,6 +71,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { var connectingDate: Date? { didSet { stateDidChange?() + resetTimeoutTimerIfNeeded() hasStartedConnectingDidChange?() } } @@ -113,12 +114,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { set { connectingDate = newValue ? Date() : nil } } - var hasConnected: Bool { + public var hasConnected: Bool { get { return connectedDate != nil } set { connectedDate = newValue ? Date() : nil } } - var hasEnded: Bool { + public var hasEnded: Bool { get { return endDate != nil } set { endDate = newValue ? Date() : nil } } @@ -277,55 +278,60 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { let duration: TimeInterval = self.duration let hasStartedConnecting: Bool = self.hasStartedConnecting - Storage.shared.writeAsync { db in - guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else { - return - } - - let updateToMissedIfNeeded: () throws -> () = { - let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed) + Storage.shared.writeAsync( + updates: { db in + guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else { + return + } - guard - let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8), - let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( - CallMessage.MessageInfo.self, - from: infoMessageData - ), - messageInfo.state == .incoming, - let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo) - else { return } + let updateToMissedIfNeeded: () throws -> () = { + let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed) + + guard + let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8), + let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( + CallMessage.MessageInfo.self, + from: infoMessageData + ), + messageInfo.state == .incoming, + let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo) + else { return } + + _ = try interaction + .with(body: String(data: missedCallInfoData, encoding: .utf8)) + .saved(db) + } + let shouldMarkAsRead: Bool = try { + if duration > 0 { return true } + if hasStartedConnecting { return true } + + switch mode { + case .local: + try updateToMissedIfNeeded() + return true + + case .remote, .unanswered: + try updateToMissedIfNeeded() + return false + + case .answeredElsewhere: return true + } + }() - _ = try interaction - .with(body: String(data: missedCallInfoData, encoding: .utf8)) - .saved(db) - } - let shouldMarkAsRead: Bool = try { - if duration > 0 { return true } - if hasStartedConnecting { return true } + guard shouldMarkAsRead else { return } - switch mode { - case .local: - try updateToMissedIfNeeded() - return true - - case .remote, .unanswered: - try updateToMissedIfNeeded() - return false - - case .answeredElsewhere: return true - } - }() - - guard shouldMarkAsRead else { return } - - try Interaction.markAsRead( - db, - interactionId: interaction.id, - threadId: interaction.threadId, - includingOlder: false, - trySendReadReceipt: false - ) - } + try Interaction.markAsRead( + db, + interactionId: interaction.id, + threadId: interaction.threadId, + includingOlder: false, + trySendReadReceipt: false + ) + }, + completion: { _, _ in + SessionCallManager.suspendDatabaseIfCallEndedInBackground() + } + ) } // MARK: - Renderer @@ -410,7 +416,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { public func setupTimeoutTimer() { invalidateTimeoutTimer() - let timeInterval: TimeInterval = (hasConnected ? 60 : 30) + let timeInterval: TimeInterval = 60 timeOutTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: timeInterval, repeats: false) { _ in self.didTimeout = true @@ -421,6 +427,11 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { } } + public func resetTimeoutTimerIfNeeded() { + if self.timeOutTimer == nil { return } + setupTimeoutTimer() + } + public func invalidateTimeoutTimer() { timeOutTimer?.invalidate() timeOutTimer = nil diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 643268bc1..38ecd7b75 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -73,13 +73,19 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - Report calls public static func reportFakeCall(info: String) { - SessionCallManager.sharedProvider(useSystemCallLog: false) - .reportNewIncomingCall( - with: UUID(), - update: CXCallUpdate() - ) { _ in - SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)") - } + let callId = UUID() + let provider = SessionCallManager.sharedProvider(useSystemCallLog: false) + provider.reportNewIncomingCall( + with: callId, + update: CXCallUpdate() + ) { _ in + SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)") + } + provider.reportCall( + with: callId, + endedAt: nil, + reason: .failed + ) } public func reportOutgoingCall(_ call: SessionCall) { @@ -98,30 +104,22 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } public func reportIncomingCall(_ call: SessionCall, callerName: String, completion: @escaping (Error?) -> Void) { - AssertIsOnMainThread() - - if let provider = provider { - // Construct a CXCallUpdate describing the incoming call, including the caller. - let update = CXCallUpdate() - update.localizedCallerName = callerName - update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString) - update.hasVideo = false + let provider = provider ?? Self.sharedProvider(useSystemCallLog: false) + // Construct a CXCallUpdate describing the incoming call, including the caller. + let update = CXCallUpdate() + update.localizedCallerName = callerName + update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString) + update.hasVideo = false - disableUnsupportedFeatures(callUpdate: update) + disableUnsupportedFeatures(callUpdate: update) - // Report the incoming call to the system - provider.reportNewIncomingCall(with: call.callId, update: update) { error in - guard error == nil else { - self.reportCurrentCallEnded(reason: .failed) - completion(error) - return - } - UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") - completion(nil) + // Report the incoming call to the system + provider.reportNewIncomingCall(with: call.callId, update: update) { error in + guard error == nil else { + self.reportCurrentCallEnded(reason: .failed) + completion(error) + return } - } - else { - SessionCallManager.reportFakeCall(info: "No CXProvider instance") UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") completion(nil) } @@ -135,7 +133,16 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { return } - guard let call = currentCall else { return } + func handleCallEnded() { + WebRTCSession.current = nil + UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + } + + guard let call = currentCall else { + handleCallEnded() + Self.suspendDatabaseIfCallEndedInBackground() + return + } if let reason = reason { self.provider?.reportCall(with: call.callId, endedAt: nil, reason: reason) @@ -153,8 +160,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { call.webRTCSession.dropConnection() self.currentCall = nil - WebRTCSession.current = nil - UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + handleCallEnded() } // MARK: - Util @@ -172,15 +178,18 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { callUpdate.supportsDTMF = false } + public static func suspendDatabaseIfCallEndedInBackground() { + if CurrentAppContext().isInBackground() { + // Stop all jobs except for message sending and when completed suspend the database + JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } + } + } + // MARK: - UI public func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) { - guard Thread.isMainThread else { - DispatchQueue.main.async { - self.showCallUIForCall(caller: caller, uuid: uuid, mode: mode, interactionId: interactionId) - } - return - } guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: caller, uuid: uuid, mode: mode) }) else { return } @@ -193,20 +202,23 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } guard CurrentAppContext().isMainAppAndActive else { return } - guard let presentingVC = CurrentAppContext().frontmostViewController() else { - preconditionFailure() // FIXME: Handle more gracefully - } - if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId { - let callVC = CallVC(for: call) - callVC.conversationVC = conversationVC - conversationVC.inputAccessoryView?.isHidden = true - conversationVC.inputAccessoryView?.alpha = 0 - presentingVC.present(callVC, animated: true, completion: nil) - } - else if !Preferences.isCallKitSupported { - let incomingCallBanner = IncomingCallBanner(for: call) - incomingCallBanner.show() + DispatchQueue.main.async { + guard let presentingVC = CurrentAppContext().frontmostViewController() else { + preconditionFailure() // FIXME: Handle more gracefully + } + + if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId { + let callVC = CallVC(for: call) + callVC.conversationVC = conversationVC + conversationVC.inputAccessoryView?.isHidden = true + conversationVC.inputAccessoryView?.alpha = 0 + presentingVC.present(callVC, animated: true, completion: nil) + } + else if !Preferences.isCallKitSupported { + let incomingCallBanner = IncomingCallBanner(for: call) + incomingCallBanner.show() + } } } } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 10748cc43..f818e421b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -315,12 +315,11 @@ extension ConversationVC: let modal = SendSeedModal() modal.modalPresentationStyle = .overFullScreen modal.modalTransitionStyle = .crossDissolve - modal.proceed = { self.sendMessage(hasPermissionToSendSeed: true) } + modal.proceed = { [weak self] in self?.sendMessage(hasPermissionToSendSeed: true) } return present(modal, animated: true, completion: nil) } - // Clearing this out immediately (even though it already happens in 'messageSent') to prevent - // "double sending" if the user rapidly taps the send button + // Clearing this out immediately to make this appear more snappy DispatchQueue.main.async { [weak self] in self?.snInputView.text = "" self?.snInputView.quoteDraftInfo = nil @@ -416,7 +415,7 @@ extension ConversationVC: ) } - func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) { + func sendAttachments(_ attachments: [SignalAttachment], with text: String, hasPermissionToSendSeed: Bool = false, onComplete: (() -> ())? = nil) { guard !showBlockedModalIfNeeded() else { return } for attachment in attachments { @@ -426,6 +425,25 @@ extension ConversationVC: } let text = replaceMentions(in: snInputView.text.trimmingCharacters(in: .whitespacesAndNewlines)) + + if text.contains(mnemonic) && !viewModel.threadData.threadIsNoteToSelf && !hasPermissionToSendSeed { + // Warn the user if they're about to send their seed to someone + let modal = SendSeedModal() + modal.modalPresentationStyle = .overFullScreen + modal.modalTransitionStyle = .crossDissolve + modal.proceed = { [weak self] in + self?.sendAttachments(attachments, with: text, hasPermissionToSendSeed: true, onComplete: onComplete) + } + return present(modal, animated: true, completion: nil) + } + + // Clearing this out immediately to make this appear more snappy + DispatchQueue.main.async { [weak self] in + self?.snInputView.text = "" + self?.snInputView.quoteDraftInfo = nil + + self?.resetMentions() + } // Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can // use it to determine if the user is creating a new thread and update the 'isApproved' @@ -492,13 +510,6 @@ extension ConversationVC: } func handleMessageSent() { - DispatchQueue.main.async { [weak self] in - self?.snInputView.text = "" - self?.snInputView.quoteDraftInfo = nil - - self?.resetMentions() - } - if Storage.shared[.playNotificationSoundInForeground] { let soundID = Preferences.Sound.systemSoundId(for: .messageSent, quiet: true) AudioServicesPlaySystemSound(soundID) @@ -920,6 +931,7 @@ extension ConversationVC: } func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) { + guard viewModel.threadData.canWrite else { return } guard SessionId.Prefix(from: sessionId) == .blinded else { Storage.shared.write { db in try SessionThread.fetchOrCreate(db, id: sessionId, variant: .contact) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 8ac1dd03c..8142600ea 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -69,10 +69,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers } override var inputAccessoryView: UIView? { - guard - viewModel.threadData.threadVariant != .closedGroup || - viewModel.threadData.currentUserIsClosedGroupMember == true - else { return nil } + guard viewModel.threadData.canWrite else { return nil } return (isShowingSearchUI ? searchController.resultsBar : snInputView) } @@ -142,10 +139,11 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers result.showsVerticalScrollIndicator = false result.contentInsetAdjustmentBehavior = .never result.keyboardDismissMode = .interactive + let bottomInset: CGFloat = viewModel.threadData.canWrite ? Values.mediumSpacing : Values.mediumSpacing + UIApplication.shared.keyWindow!.safeAreaInsets.bottom result.contentInset = UIEdgeInsets( top: 0, leading: 0, - bottom: Values.mediumSpacing, + bottom: bottomInset, trailing: 0 ) result.registerHeaderFooterView(view: UITableViewHeaderFooterView.self) @@ -894,7 +892,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers deleteSectionsAnimation: .none, insertSectionsAnimation: .none, reloadSectionsAnimation: .none, - deleteRowsAnimation: .bottom, + deleteRowsAnimation: .fade, insertRowsAnimation: .none, reloadRowsAnimation: .none, interrupt: { itemChangeInfo?.isInsertAtTop == true || $0.changeCount > ConversationViewModel.pageSize } diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 36e0ced6e..0037820e4 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -47,12 +47,24 @@ final class CallMessageCell: MessageCell { private lazy var container: UIView = { let result: UIView = UIView() - result.set(.height, to: 50) result.layer.cornerRadius = 18 result.backgroundColor = Colors.callMessageBackground result.addSubview(label) - label.autoCenterInSuperview() + label.pin(.top, to: .top, of: result, withInset: CallMessageCell.inset) + label.pin( + .left, + to: .left, + of: result, + withInset: ((CallMessageCell.inset * 2) + infoImageView.bounds.size.width) + ) + label.pin( + .right, + to: .right, + of: result, + withInset: -((CallMessageCell.inset * 2) + infoImageView.bounds.size.width) + ) + label.pin(.bottom, to: .bottom, of: result, withInset: -CallMessageCell.inset) result.addSubview(iconImageView) iconImageView.autoVCenterInSuperview() diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 79db24db8..92e0295c0 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -78,6 +78,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD SNAppearance.switchToSessionAppearance() + if Environment.shared?.callManager.wrappedValue?.currentCall == nil { + UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + } + // No point continuing if we are running tests guard !CurrentAppContext().isRunningTests else { return true } @@ -132,21 +136,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // NOTE: Fix an edge case where user taps on the callkit notification // but answers the call on another device - stopPollers(shouldStopUserPoller: !self.hasIncomingCallWaiting()) + stopPollers(shouldStopUserPoller: !self.hasCallOngoing()) // Stop all jobs except for message sending and when completed suspend the database JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + if !self.hasCallOngoing() { + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } } } func applicationDidReceiveMemoryWarning(_ application: UIApplication) { Logger.info("applicationDidReceiveMemoryWarning") } - + func applicationWillTerminate(_ application: UIApplication) { DDLog.flushLog() - + stopPollers() } @@ -244,16 +250,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Configuration.performMainSetup() JobRunner.add(executor: SyncPushTokensJob.self, for: .syncPushTokens) - // Trigger any launch-specific jobs and start the JobRunner - JobRunner.appDidFinishLaunching() - /// Setup the UI /// - /// **Note:** This **MUST** be run before calling `AppReadiness.setAppIsReady()` otherwise if - /// we are launching the app from a push notification the HomeVC won't be setup yet and it won't open the - /// related thread + /// **Note:** This **MUST** be run before calling: + /// - `AppReadiness.setAppIsReady()`: + /// If we are launching the app from a push notification the HomeVC won't be setup yet + /// and it won't open the related thread + /// + /// - `JobRunner.appDidFinishLaunching()`: + /// The jobs which run on launch (eg. DisappearingMessages job) can impact the interactions + /// which get fetched to display on the home screen, if the PagedDatabaseObserver hasn't + /// been setup yet then the home screen can show stale (ie. deleted) interactions incorrectly self.ensureRootViewController(isPreAppReadyCall: true) + // Trigger any launch-specific jobs and start the JobRunner + JobRunner.appDidFinishLaunching() + // Note that this does much more than set a flag; // it will also run all deferred blocks (including the JobRunner // 'appDidBecomeActive' method) @@ -634,6 +646,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return !call.hasStartedConnecting } + func hasCallOngoing() -> Bool { + guard let call = AppEnvironment.shared.callManager.currentCall else { return false } + + return !call.hasEnded + } + func handleAppActivatedWithOngoingCallIfNeeded() { guard let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall), diff --git a/Session/Meta/AudioFiles/messageReceivedSounds/silence.aiff b/Session/Meta/AudioFiles/messageReceivedSounds/silence.aiff new file mode 100644 index 000000000..c968a3c03 Binary files /dev/null and b/Session/Meta/AudioFiles/messageReceivedSounds/silence.aiff differ diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index e0c25749b..d2e5a0829 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -313,21 +313,25 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { AppNotificationUserInfoKey.threadId: thread.id ] - let notificationTitle: String = interaction.previewText(db) - let threadName: String = SessionThread.displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: nil, // Not supported - openGroupName: nil // Not supported - ) - var notificationBody: String? + let notificationTitle: String = "Session" + let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + let notificationBody: String? = { + switch messageInfo.state { + case .permissionDenied: + return String( + format: "modal_call_missed_tips_explanation".localized(), + senderName + ) + case .missed: + return String( + format: "call_missed".localized(), + senderName + ) + default: + return nil + } + }() - if messageInfo.state == .permissionDenied { - notificationBody = String( - format: "modal_call_missed_tips_explanation".localized(), - threadName - ) - } let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] .defaulting(to: Preferences.Sound.defaultNotificationSound) @@ -345,7 +349,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { previewType: previewType, sound: sound, threadVariant: thread.variant, - threadName: threadName, + threadName: senderName, replacingIdentifier: UUID().uuidString ) } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 1cd8d71cf..c2df1a4f9 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -6,7 +6,7 @@ import Foundation import PromiseKit import PushKit import SignalUtilitiesKit -import SignalUtilitiesKit +import GRDB public enum PushRegistrationError: Error { case assertionError(description: String) @@ -251,6 +251,9 @@ public enum PushRegistrationError: Error { return } + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) + let maybeCall: SessionCall? = Storage.shared.write { db in let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( state: (caller == getUserHexEncodedPublicKey(db) ? @@ -259,7 +262,13 @@ public enum PushRegistrationError: Error { ) ) - guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil } + let messageInfoString: String? = { + if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) { + return String(data: messageInfoData, encoding: .utf8) + } else { + return "Incoming call." // TODO: We can do better here. + } + }() let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact) @@ -269,7 +278,7 @@ public enum PushRegistrationError: Error { threadId: thread.id, authorId: caller, variant: .infoCall, - body: String(data: messageInfoData, encoding: .utf8), + body: messageInfoString, timestampMs: timestampMs ).inserted(db) call.callInteractionId = interaction.id diff --git a/SessionMessagingKit/Calls/CurrentCallProtocol.swift b/SessionMessagingKit/Calls/CurrentCallProtocol.swift index 6968116db..8cdaf7ff7 100644 --- a/SessionMessagingKit/Calls/CurrentCallProtocol.swift +++ b/SessionMessagingKit/Calls/CurrentCallProtocol.swift @@ -9,6 +9,7 @@ public protocol CurrentCallProtocol { var callId: UUID { get } var webRTCSession: WebRTCSession { get } var hasStartedConnecting: Bool { get set } + var hasEnded: Bool { get set } func updateCallMessage(mode: EndCallMode) func didReceiveRemoteSDP(sdp: RTCSessionDescription) diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 54e1b0258..aa7ae21c2 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -22,7 +22,8 @@ public enum SNMessagingKit { // Just to make the external API nice _007_HomeQueryOptimisationIndexes.self ], [ - _008_EmojiReacts.self + _008_EmojiReacts.self, + _009_OpenGroupPermission.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift b/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift new file mode 100644 index 000000000..4f6036a2d --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift @@ -0,0 +1,27 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +enum _009_OpenGroupPermission: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "OpenGroupPermission" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.1 + + static func migrate(_ db: GRDB.Database) throws { + try db.alter(table: OpenGroup.self) { t in + t.add(.permissions, .integer) + .defaults(to: OpenGroup.Permissions.all) + } + + // When modifying OpenGroup behaviours we should always look to reset the `infoUpdates` + // value for all OpenGroups to ensure they all have the correct state for newly + // added/changed fields + _ = try OpenGroup + .updateAll(db, OpenGroup.Columns.infoUpdates.set(to: 0)) + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 0b9c64e96..3179e5fdc 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -523,8 +523,13 @@ public extension Interaction { .asRequest(of: Int64.self) .fetchAll(db) - // Don't bother continuing if there are not interactions to mark as read - guard !interactionIdsToMarkAsRead.isEmpty else { return } + // If there are no other interactions to mark as read then just schedule the jobs + // for this interaction (need to ensure the disapeparing messages run for sync'ed + // outgoing messages which will always have 'wasRead' as false) + guard !interactionIdsToMarkAsRead.isEmpty else { + scheduleJobs(interactionIds: [interactionId]) + return + } // Update the `wasRead` flag to true try interactionQuery.updateAll(db, Columns.wasRead.set(to: true)) diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index d2aae5f69..48112352c 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -27,6 +27,38 @@ public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableReco case inboxLatestMessageId case outboxLatestMessageId case pollFailureCount + case permissions + } + + public struct Permissions: OptionSet, Codable, DatabaseValueConvertible, Hashable { + public let rawValue: UInt16 + + public init(rawValue: UInt16) { + self.rawValue = rawValue + } + + public init(roomInfo: OpenGroupAPI.RoomPollInfo) { + var permissions: Permissions = [] + + if roomInfo.read { permissions.insert(.read) } + if roomInfo.write { permissions.insert(.write) } + if roomInfo.upload { permissions.insert(.upload) } + + self.init(rawValue: permissions.rawValue) + } + + public func toString() -> String { + return "" + .appending(self.contains(.read) ? "r" : "-") + .appending(self.contains(.write) ? "w" : "-") + .appending(self.contains(.upload) ? "u" : "-") + } + + static let read: Permissions = Permissions(rawValue: 1 << 0) + static let write: Permissions = Permissions(rawValue: 1 << 1) + static let upload: Permissions = Permissions(rawValue: 1 << 2) + + static let all: Permissions = [ .read, .write, .upload ] } public var id: String { threadId } // Identifiable @@ -90,6 +122,9 @@ public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableReco /// The number of times this room has failed to poll since the last successful poll public let pollFailureCount: Int64 + /// The permissions this room has for current user + public let permissions: Permissions? + // MARK: - Relationships public var thread: QueryInterfaceRequest { @@ -122,7 +157,8 @@ public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableReco sequenceNumber: Int64 = 0, inboxLatestMessageId: Int64 = 0, outboxLatestMessageId: Int64 = 0, - pollFailureCount: Int64 = 0 + pollFailureCount: Int64 = 0, + permissions: Permissions? = nil ) { self.threadId = OpenGroup.idFor(roomToken: roomToken, server: server) self.server = server.lowercased() @@ -139,6 +175,7 @@ public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableReco self.inboxLatestMessageId = inboxLatestMessageId self.outboxLatestMessageId = outboxLatestMessageId self.pollFailureCount = pollFailureCount + self.permissions = permissions } } @@ -166,7 +203,8 @@ public extension OpenGroup { sequenceNumber: 0, inboxLatestMessageId: 0, outboxLatestMessageId: 0, - pollFailureCount: 0 + pollFailureCount: 0, + permissions: nil ) } @@ -200,7 +238,8 @@ extension OpenGroup: CustomStringConvertible, CustomDebugStringConvertible { "sequenceNumber: \(sequenceNumber)", "inboxLatestMessageId: \(inboxLatestMessageId)", "outboxLatestMessageId: \(outboxLatestMessageId)", - "pollFailureCount: \(pollFailureCount))" + "pollFailureCount: \(pollFailureCount))", + "permissions: \(permissions?.toString() ?? "---")" ].joined(separator: ", ") } } diff --git a/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift b/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift index 9ed31c7e7..d6897e1ff 100644 --- a/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift +++ b/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift @@ -17,7 +17,7 @@ public enum DisappearingMessagesJob: JobExecutor { deferred: @escaping (Job) -> () ) { // The 'backgroundTask' gets captured and cleared within the 'completion' block - let timestampNowMs: TimeInterval = (Date().timeIntervalSince1970 * 1000) + let timestampNowMs: TimeInterval = ceil(Date().timeIntervalSince1970 * 1000) var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: #function) let updatedJob: Job? = Storage.shared.write { db in @@ -29,12 +29,9 @@ public enum DisappearingMessagesJob: JobExecutor { // Update the next run timestamp for the DisappearingMessagesJob (if the call // to 'updateNextRunIfNeeded' returns 'nil' then it doesn't need to re-run so // should have it's 'nextRunTimestamp' cleared) - return updateNextRunIfNeeded(db) - .defaulting( - to: try job - .with(nextRunTimestamp: 0) - .saved(db) - ) + return try updateNextRunIfNeeded(db) + .defaulting(to: job.with(nextRunTimestamp: 0)) + .saved(db) } success(updatedJob ?? job, false) @@ -65,7 +62,7 @@ public extension DisappearingMessagesJob { return try? Job .filter(Job.Columns.variant == Job.Variant.disappearingMessages) .fetchOne(db)? - .with(nextRunTimestamp: ((nextExpirationTimestampMs / 1000) + 1)) + .with(nextRunTimestamp: ceil(nextExpirationTimestampMs / 1000)) .saved(db) } diff --git a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift index a8160efe1..9f2214fe9 100644 --- a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift @@ -369,9 +369,15 @@ public extension ClosedGroupControlMessage.Kind { return String(format: "GROUP_TITLE_CHANGED".localized(), name) case .membersAdded(let membersAsData): - let addedMemberNames: [String] = try Profile - .fetchAll(db, ids: membersAsData.map { $0.toHexString() }) - .map { $0.displayName() } + let memberIds: [String] = membersAsData.map { $0.toHexString() } + let knownMemberNameMap: [String: String] = try Profile + .fetchAll(db, ids: memberIds) + .reduce(into: [:]) { result, next in result[next.id] = next.displayName() } + let addedMemberNames: [String] = memberIds + .map { + knownMemberNameMap[$0] ?? + Profile.truncated(id: $0, threadVariant: .closedGroup) + } return String( format: "GROUP_MEMBER_JOINED".localized(), @@ -387,9 +393,14 @@ public extension ClosedGroupControlMessage.Kind { var infoMessage: String = "" if !memberIds.removing(userPublicKey).isEmpty { - let removedMemberNames: [String] = try Profile + let knownMemberNameMap: [String: String] = try Profile .fetchAll(db, ids: memberIds.removing(userPublicKey)) - .map { $0.displayName() } + .reduce(into: [:]) { result, next in result[next.id] = next.displayName() } + let removedMemberNames: [String] = memberIds.removing(userPublicKey) + .map { + knownMemberNameMap[$0] ?? + Profile.truncated(id: $0, threadVariant: .closedGroup) + } let format: String = (removedMemberNames.count > 1 ? "GROUP_MEMBERS_REMOVED".localized() : "GROUP_MEMBER_REMOVED".localized() diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 7afea37a0..71ec4b58e 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -384,6 +384,8 @@ public final class OpenGroupManager: NSObject { // Only update the database columns which have changed (this is to prevent the UI from triggering // updates due to changing database columns to the existing value) + let permissions = OpenGroup.Permissions(roomInfo: pollInfo) + try OpenGroup .filter(id: openGroup.id) .updateAll( @@ -412,6 +414,10 @@ public final class OpenGroupManager: NSObject { (openGroup.infoUpdates != pollInfo.details?.infoUpdates ? (pollInfo.details?.infoUpdates).map { OpenGroup.Columns.infoUpdates.set(to: $0) } : nil + ), + (openGroup.permissions != permissions ? + OpenGroup.Columns.permissions.set(to: permissions) : + nil ) ].compactMap { $0 } ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 20d52e23f..5c5620a81 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -377,7 +377,7 @@ extension MessageReceiver { .membersRemoved( members: removedMembers .asSet() - .subtracting(groupMembers.map { $0.profileId }) + .intersection(groupMembers.map { $0.profileId }) .map { Data(hex: $0) } ) .infoMessage(db, sender: sender), diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 10018d5e1..12e15fc9c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -340,13 +340,14 @@ extension MessageReceiver { sortId: sortId ) try reaction.insert(db) - Environment.shared?.notificationsManager.wrappedValue? - .notifyUser( - db, - forReaction: reaction, - in: thread - ) - + if sender != getUserHexEncodedPublicKey(db) { + Environment.shared?.notificationsManager.wrappedValue? + .notifyUser( + db, + forReaction: reaction, + in: thread + ) + } case .remove: try Reaction .filter(Reaction.Columns.interactionId == interactionId) diff --git a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift index cdda8b025..9e50e80b8 100644 --- a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift @@ -98,7 +98,7 @@ public class TypingIndicators { withTimeInterval: (direction == .outgoing ? 3 : 5), repeats: false ) { _ in - Storage.shared.write { db in + Storage.shared.writeAsync { db in TypingIndicators.didStopTyping(db, threadId: threadId, direction: direction) } } @@ -123,7 +123,7 @@ public class TypingIndicators { withTimeInterval: 10, repeats: false ) { [weak self] _ in - Storage.shared.write { db in + Storage.shared.writeAsync { db in self?.scheduleRefreshCallback(db) } } diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 3d2ca2867..4e79e3917 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -45,6 +45,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat public static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) public static let openGroupProfilePictureDataKey: SQL = SQL(stringLiteral: CodingKeys.openGroupProfilePictureData.stringValue) public static let openGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.openGroupUserCount.stringValue) + public static let openGroupPermissionsKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPermissions.stringValue) public static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) public static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) public static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) @@ -94,6 +95,14 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat public let threadUnreadCount: UInt? public let threadUnreadMentionCount: UInt? + public var canWrite: Bool { + switch threadVariant { + case .contact: return true + case .closedGroup: return currentUserIsClosedGroupMember == true + case .openGroup: return openGroupPermissions?.contains(.write) ?? false + } + } + // Thread display info private let contactProfile: Profile? @@ -109,6 +118,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat public let openGroupRoomToken: String? public let openGroupProfilePictureData: Data? private let openGroupUserCount: Int? + private let openGroupPermissions: OpenGroup.Permissions? // Interaction display info @@ -262,6 +272,7 @@ public extension SessionThreadViewModel { self.openGroupRoomToken = nil self.openGroupProfilePictureData = nil self.openGroupUserCount = nil + self.openGroupPermissions = nil // Interaction display info @@ -320,6 +331,7 @@ public extension SessionThreadViewModel { openGroupRoomToken: self.openGroupRoomToken, openGroupProfilePictureData: self.openGroupProfilePictureData, openGroupUserCount: self.openGroupUserCount, + openGroupPermissions: self.openGroupPermissions, interactionId: self.interactionId, interactionVariant: self.interactionVariant, interactionTimestampMs: self.interactionTimestampMs, @@ -371,6 +383,7 @@ public extension SessionThreadViewModel { openGroupRoomToken: self.openGroupRoomToken, openGroupProfilePictureData: self.openGroupProfilePictureData, openGroupUserCount: self.openGroupUserCount, + openGroupPermissions: self.openGroupPermissions, interactionId: self.interactionId, interactionVariant: self.interactionVariant, interactionTimestampMs: self.interactionTimestampMs, @@ -726,6 +739,7 @@ public extension SessionThreadViewModel { \(openGroup[.server]) AS \(ViewModel.openGroupServerKey), \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), \(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey), + \(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey), \(Interaction.self).\(ViewModel.interactionIdKey), diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index 144133dff..1b406c118 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -232,7 +232,7 @@ public enum Preferences { // Other case .messageSent: return "message_sent.aiff" - case .none: return nil + case .none: return "silence.aiff" } } diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 62956ff80..686e6109f 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -157,18 +157,15 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { notificationContent.badge = NSNumber(value: newBadgeNumber) CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber") - notificationContent.title = interaction.previewText(db) + notificationContent.title = "Session" notificationContent.body = "" + let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + if messageInfo.state == .permissionDenied { notificationContent.body = String( format: "modal_call_missed_tips_explanation".localized(), - SessionThread.displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: nil, // Not supported - openGroupName: nil // Not supported - ) + senderName ) } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 5705a4661..7cfeae747 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -25,6 +25,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension self.contentHandler = contentHandler self.request = request + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) + guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { return self.completeSilenty() } @@ -237,6 +240,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension private func completeSilenty() { SNLog("Complete silenty") + + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + self.contentHandler!(.init()) } @@ -298,11 +305,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension SNLog("Add remote notification request") } - private func handleSuccess(for content: UNMutableNotificationContent) { - contentHandler!(content) - } - private func handleFailure(for content: UNMutableNotificationContent) { + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + content.body = "You've got a new message" content.title = "Session" let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ] diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 4d3352558..f1ea3813d 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -220,6 +220,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView shareVC?.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) Storage.shared .writeAsync { [weak self] db -> Promise in guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { @@ -271,10 +273,14 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView ) } .done { [weak self] _ in + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) activityIndicator.dismiss { } self?.shareVC?.shareViewWasCompleted() } .catch { [weak self] error in + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) activityIndicator.dismiss { } self?.shareVC?.shareViewFailed(error: error) } diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index d336f624b..2d08e817a 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -888,30 +888,36 @@ private final class JobQueue { // but we want at least 1 second to pass before doing so - the job itself should // really update it's own 'nextRunTimestamp' (this is just a safety net) case .recurring where job.nextRunTimestamp <= Date().timeIntervalSince1970: + guard let jobId: Int64 = job.id else { break } + Storage.shared.write { db in - _ = try job - .with(nextRunTimestamp: (Date().timeIntervalSince1970 + 1)) - .saved(db) + _ = try Job + .filter(id: jobId) + .updateAll( + db, + Job.Columns.failureCount.set(to: 0), + Job.Columns.nextRunTimestamp.set(to: (Date().timeIntervalSince1970 + 1)) + ) } - // For `recurringOnLaunch/Active` jobs which have already run, we want to clear their - // `failureCount` and `nextRunTimestamp` to prevent them from endlessly running over - // and over and reset their retry backoff in case they fail next time + // For `recurringOnLaunch/Active` jobs which have already run but failed once, we need to + // clear their `failureCount` and `nextRunTimestamp` to prevent them from endlessly running + // over and over again case .recurringOnLaunch, .recurringOnActive: - if + guard let jobId: Int64 = job.id, job.failureCount != 0 && job.nextRunTimestamp > TimeInterval.leastNonzeroMagnitude - { - Storage.shared.write { db in - _ = try Job - .filter(id: jobId) - .updateAll( - db, - Job.Columns.failureCount.set(to: 0), - Job.Columns.nextRunTimestamp.set(to: 0) - ) - } + else { break } + + Storage.shared.write { db in + _ = try Job + .filter(id: jobId) + .updateAll( + db, + Job.Columns.failureCount.set(to: 0), + Job.Columns.nextRunTimestamp.set(to: 0) + ) } default: break