diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1770cceb3..7b61aecef 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7173,7 +7173,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 389; + CURRENT_PROJECT_VERSION = 390; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7212,7 +7212,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.5; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7245,7 +7245,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 389; + CURRENT_PROJECT_VERSION = 390; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7284,7 +7284,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.5; 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 1fc8620a5..a9ccbcdf0 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -206,7 +206,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else { return } - let timestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) + let timestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() let message: CallMessage = CallMessage( uuid: self.uuid, kind: .preOffer, diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 49ed7ae16..1d9f66159 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -9,15 +9,17 @@ import SessionMessagingKit import SignalUtilitiesKit private protocol TableViewTouchDelegate { - func tableViewWasTouched(_ tableView: TableView) + func tableViewWasTouched(_ tableView: TableView, withView hitView: UIView?) } private final class TableView: UITableView { var touchDelegate: TableViewTouchDelegate? override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - touchDelegate?.tableViewWasTouched(self) - return super.hitTest(point, with: event) + let resultingView: UIView? = super.hitTest(point, with: event) + touchDelegate?.tableViewWasTouched(self, withView: resultingView) + + return resultingView } } @@ -275,10 +277,23 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate ) } - fileprivate func tableViewWasTouched(_ tableView: TableView) { + fileprivate func tableViewWasTouched(_ tableView: TableView, withView hitView: UIView?) { if nameTextField.isFirstResponder { nameTextField.resignFirstResponder() } + else if searchBar.isFirstResponder { + var hitSuperview: UIView? = hitView?.superview + + while hitSuperview != nil && hitSuperview != searchBar { + hitSuperview = hitSuperview?.superview + } + + // If the user hit the cancel button then do nothing (we want to let the cancel + // button remove the focus or it will instantly refocus) + if hitSuperview == searchBar { return } + + searchBar.resignFirstResponder() + } } @objc private func close() { diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 7fe20ec66..5eb63648a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -409,7 +409,7 @@ extension ConversationVC: // flags appropriately let threadId: String = self.viewModel.threadData.threadId let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true) - let sentTimestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000))) + let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() let linkPreviewDraft: LinkPreviewDraft? = snInputView.linkPreviewInfo?.draft let quoteModel: QuotedReplyModel? = snInputView.quoteDraftInfo?.model @@ -534,7 +534,7 @@ extension ConversationVC: // flags appropriately let threadId: String = self.viewModel.threadData.threadId let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true) - let sentTimestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000))) + let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() // If this was a message request then approve it approveMessageRequestIfNeeded( @@ -640,7 +640,7 @@ extension ConversationVC: threadVariant: threadVariant, threadIsMessageRequest: threadIsMessageRequest, direction: .outgoing, - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) + timestampMs: SnodeAPI.currentOffsetTimestampMs() ) if needsToStartTypingIndicator { @@ -1219,7 +1219,7 @@ extension ConversationVC: guard !threadIsMessageRequest else { return } // Perform local rate limiting (don't allow more than 20 reactions within 60 seconds) - let sentTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) + let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs() let recentReactionTimestamps: [Int64] = General.cache.wrappedValue.recentReactionTimestamps guard @@ -2049,7 +2049,7 @@ extension ConversationVC: // Create URL let directory: String = OWSTemporaryDirectory() - let fileName: String = "\(Int64(floor(Date().timeIntervalSince1970 * 1000))).m4a" + let fileName: String = "\(SnodeAPI.currentOffsetTimestampMs()).m4a" let url: URL = URL(fileURLWithPath: directory).appendingPathComponent(fileName) // Set up audio session @@ -2290,7 +2290,7 @@ extension ConversationVC { for: self.viewModel.threadData.threadId, threadVariant: self.viewModel.threadData.threadVariant, isNewThread: false, - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) + timestampMs: SnodeAPI.currentOffsetTimestampMs() ) } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index d3c62390d..952319d9e 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -142,18 +142,33 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { private lazy var reactionContainerView = ReactionContainerView() + internal lazy var messageStatusContainerView: UIView = { + let result = UIView() + + return result + }() + + internal lazy var messageStatusLabel: UILabel = { + let result = UILabel() + result.accessibilityLabel = "Message sent status" + result.font = .systemFont(ofSize: Values.verySmallFontSize) + result.themeTextColor = .messageBubble_deliveryStatus + + return result + }() + internal lazy var messageStatusImageView: UIImageView = { let result = UIImageView() result.accessibilityLabel = "Message sent status tick" result.contentMode = .scaleAspectFit - result.layer.cornerRadius = VisibleMessageCell.messageStatusImageViewSize / 2 - result.layer.masksToBounds = true + result.themeTintColor = .messageBubble_deliveryStatus + return result }() // MARK: - Settings - private static let messageStatusImageViewSize: CGFloat = 16 + private static let messageStatusImageViewSize: CGFloat = 12 private static let authorLabelBottomSpacing: CGFloat = 4 private static let groupThreadHSpacing: CGFloat = 12 private static let profilePictureSize = Values.verySmallProfilePictureSize @@ -236,13 +251,22 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { underBubbleStackView.pin(.bottom, to: .bottom, of: self) underBubbleStackView.addArrangedSubview(reactionContainerView) - underBubbleStackView.addArrangedSubview(messageStatusImageView) + underBubbleStackView.addArrangedSubview(messageStatusContainerView) + + messageStatusContainerView.addSubview(messageStatusLabel) + messageStatusContainerView.addSubview(messageStatusImageView) reactionContainerView.widthAnchor .constraint(lessThanOrEqualTo: underBubbleStackView.widthAnchor) .isActive = true + messageStatusImageView.pin(.top, to: .top, of: messageStatusContainerView) + messageStatusImageView.pin(.bottom, to: .bottom, of: messageStatusContainerView) + messageStatusImageView.pin(.trailing, to: .trailing, of: messageStatusContainerView) messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) + messageStatusLabel.center(.vertical, in: messageStatusContainerView) + messageStatusLabel.pin(.leading, to: .leading, of: messageStatusContainerView) + messageStatusLabel.pin(.trailing, to: .leading, of: messageStatusImageView, withInset: -2) } override func setUpGestureRecognizers() { @@ -389,13 +413,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ) // Message status image view - let (image, tintColor) = cellViewModel.state.statusIconInfo( + let (image, statusText, tintColor) = cellViewModel.state.statusIconInfo( variant: cellViewModel.variant, hasAtLeastOneReadReceipt: cellViewModel.hasAtLeastOneReadReceipt ) + messageStatusLabel.text = statusText + messageStatusLabel.themeTextColor = tintColor messageStatusImageView.image = image messageStatusImageView.themeTintColor = tintColor - messageStatusImageView.isHidden = ( + messageStatusContainerView.isHidden = ( cellViewModel.variant != .standardOutgoing || cellViewModel.variant == .infoCall || ( diff --git a/Session/Conversations/Settings/OWSMessageTimerView.m b/Session/Conversations/Settings/OWSMessageTimerView.m index dc71b3b06..bfe57d7e3 100644 --- a/Session/Conversations/Settings/OWSMessageTimerView.m +++ b/Session/Conversations/Settings/OWSMessageTimerView.m @@ -8,6 +8,7 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -75,7 +76,7 @@ const CGFloat kDisappearingMessageIconSize = 12.f; return; } - uint64_t nowTimestamp = [NSDate ows_millisecondTimeStamp]; + uint64_t nowTimestamp = [SNSnodeAPI currentOffsetTimestampMs]; CGFloat secondsLeft = (self.expirationTimestamp > nowTimestamp ? (self.expirationTimestamp - nowTimestamp) / 1000.f : 0.f); CGFloat progress = 0.f; diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesViewModel.swift index aace471aa..886cc326a 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesViewModel.swift @@ -168,7 +168,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel = Set() + private var onFooterTap: (() -> ())? public var viewModelType: AnyObject.Type { return type(of: viewModel) } @@ -45,6 +46,30 @@ class SessionTableViewController) { @@ -74,6 +99,8 @@ class SessionTableViewController Int { diff --git a/Session/Shared/SessionTableViewModel.swift b/Session/Shared/SessionTableViewModel.swift index b3cc0186e..88a1c132b 100644 --- a/Session/Shared/SessionTableViewModel.swift +++ b/Session/Shared/SessionTableViewModel.swift @@ -43,6 +43,9 @@ class SessionTableViewModel { Just(nil).eraseToAnyPublisher() } + open var footerButtonInfo: AnyPublisher { + Just(nil).eraseToAnyPublisher() + } func updateSettings(_ updatedSettings: [SectionModel]) { preconditionFailure("abstract class - override in subclass") diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index f6e4bf48a..e9c789158 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -5,6 +5,7 @@ import GRDB import PromiseKit import WebRTC import SessionUtilitiesKit +import SessionSnodeKit public protocol WebRTCSessionDelegate: AnyObject { var videoCapturer: RTCVideoCapturer { get } @@ -179,7 +180,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { uuid: uuid, kind: .offer, sdps: [ sdp.sdp ], - sentTimestampMs: UInt64(floor(Date().timeIntervalSince1970 * 1000)) + sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()) ), interactionId: nil, in: thread diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index d87a10ef2..f065d6795 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -1286,7 +1286,7 @@ enum _003_YDBToGRDBMigration: Migration { // so we can reverse-engineer an approximate timestamp by extracting it from // the id (this value is unlikely to match exactly though) let fallbackTimestamp: UInt64 = legacyJob.id - .map { UInt64($0.prefix("\(Int(Date().timeIntervalSince1970 * 1000))".count)) } + .map { UInt64($0.prefix("\(SnodeAPI.currentOffsetTimestampMs())".count)) } .defaulting(to: 0) let legacyIdentifier: String = identifier( for: threadId, @@ -1657,7 +1657,7 @@ enum _003_YDBToGRDBMigration: Migration { state: .invalid, contentType: "", byteCount: 0, - creationTimestamp: Date().timeIntervalSince1970, + creationTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), sourceFilename: nil, downloadUrl: nil, localRelativeFilePath: nil, diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index a62849bd7..72b8ff0c6 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -1,12 +1,13 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import AVFAudio +import AVFoundation import GRDB import PromiseKit import SignalCoreKit import SessionUtilitiesKit -import AVFAudio -import AVFoundation +import SessionSnodeKit public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "attachment" } @@ -1062,7 +1063,7 @@ extension Attachment { // Check the file size SNLog("File size: \(data.count) bytes.") - if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier { + if data.count > FileServerAPI.maxFileSize { failure?(HTTP.Error.maxFileSizeExceeded) return } @@ -1114,7 +1115,7 @@ extension Attachment { state: .uploaded, creationTimestamp: ( updatedAttachment?.creationTimestamp ?? - Date().timeIntervalSince1970 + (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) ), downloadUrl: "\(FileServerAPI.server)/files/\(fileId)" ) diff --git a/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift b/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift index cafc4645f..c7cd38636 100644 --- a/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift +++ b/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift @@ -3,6 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit +import SessionSnodeKit /// We can rely on the unique constraints within the `Interaction` table to prevent duplicate `VisibleMessage` /// values from being processed, but some control messages don’t have an associated interaction - this table provides @@ -168,7 +169,10 @@ internal extension ControlMessageProcessRecord { self.threadId = threadId self.timestampMs = timestampMs - self.serverExpirationTimestamp = (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds) + self.serverExpirationTimestamp = ( + (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) + + ControlMessageProcessRecord.defaultExpirationSeconds + ) } /// This method should only be used for records created during migration from the legacy @@ -179,7 +183,8 @@ internal extension ControlMessageProcessRecord { /// clean out these excessive entries after `defaultExpirationSeconds`) static func generateLegacyProcessRecords(_ db: Database, receivedMessageTimestamps: [Int64]) throws { let defaultExpirationTimestamp: TimeInterval = ( - Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds + (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) + + ControlMessageProcessRecord.defaultExpirationSeconds ) try receivedMessageTimestamps.forEach { timestampMs in diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift index fbbf24242..963e2c65e 100644 --- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift +++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift @@ -3,6 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit +import SessionSnodeKit public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "disappearingMessagesConfiguration" } @@ -206,7 +207,7 @@ public class SMKDisappearingMessagesConfiguration: NSObject { authorId: getUserHexEncodedPublicKey(db), variant: .infoDisappearingMessagesUpdate, body: config.messageInfoString(with: nil), - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) + timestampMs: SnodeAPI.currentOffsetTimestampMs() ) .inserted(db) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 0f9ed331f..83782c4b7 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import Sodium import SessionUtilitiesKit +import SessionSnodeKit public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "interaction" } @@ -298,7 +299,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu self.timestampMs = timestampMs self.receivedAtTimestampMs = { switch variant { - case .standardIncoming, .standardOutgoing: return Int64(Date().timeIntervalSince1970 * 1000) + case .standardIncoming, .standardOutgoing: return SnodeAPI.currentOffsetTimestampMs() /// For TSInteractions which are not `standardIncoming` and `standardOutgoing` use the `timestampMs` value default: return timestampMs @@ -458,7 +459,7 @@ public extension Interaction { job: DisappearingMessagesJob.updateNextRunIfNeeded( db, interactionIds: interactionIds, - startedAtMs: (Date().timeIntervalSince1970 * 1000) + startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs()) ) ) diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index 21b3de9fa..46b99ff74 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -6,6 +6,7 @@ import PromiseKit import AFNetworking import SignalCoreKit import SessionUtilitiesKit +import SessionSnodeKit public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "linkPreview" } @@ -60,7 +61,7 @@ public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, Persis public init( url: String, timestamp: TimeInterval = LinkPreview.timestampFor( - sentTimestampMs: (Date().timeIntervalSince1970 * 1000) // Default to now + sentTimestampMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs()) // Default to now ), variant: Variant = .standard, title: String?, diff --git a/SessionMessagingKit/Database/Models/RecipientState.swift b/SessionMessagingKit/Database/Models/RecipientState.swift index bed9a84f2..929a5cb8c 100644 --- a/SessionMessagingKit/Database/Models/RecipientState.swift +++ b/SessionMessagingKit/Database/Models/RecipientState.swift @@ -65,16 +65,37 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe } } - public func statusIconInfo(variant: Interaction.Variant, hasAtLeastOneReadReceipt: Bool) -> (image: UIImage?, themeTintColor: ThemeValue) { - guard variant == .standardOutgoing else { return (nil, .textPrimary) } + public func statusIconInfo(variant: Interaction.Variant, hasAtLeastOneReadReceipt: Bool) -> (image: UIImage?, text: String?, themeTintColor: ThemeValue) { + guard variant == .standardOutgoing else { return (nil, nil, .textPrimary) } switch (self, hasAtLeastOneReadReceipt) { - case (.sending, _): return (UIImage(systemName: "ellipsis.circle"), .textPrimary) + case (.sending, _): + return ( + UIImage(systemName: "ellipsis.circle"), + "MESSAGE_DELIVERY_STATUS_SENDING".localized(), + .messageBubble_deliveryStatus + ) + case (.sent, false), (.skipped, _): - return (UIImage(systemName: "checkmark.circle"), .textPrimary) + return ( + UIImage(systemName: "checkmark.circle"), + "MESSAGE_DELIVERY_STATUS_SENT".localized(), + .messageBubble_deliveryStatus + ) + + case (.sent, true): + return ( + UIImage(systemName: "eye.fill"), + "MESSAGE_DELIVERY_STATUS_READ".localized(), + .messageBubble_deliveryStatus + ) - case (.sent, true): return (UIImage(systemName: "checkmark.circle.fill"), .textPrimary) - case (.failed, _): return (UIImage(systemName: "exclamationmark.circle"), .danger) + case (.failed, _): + return ( + UIImage(systemName: "exclamationmark.triangle"), + "MESSAGE_DELIVERY_STATUS_FAILED".localized(), + .danger + ) } } } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index b19805072..ca08aa182 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import Sodium import SessionUtilitiesKit +import SessionSnodeKit public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public static var databaseTableName: String { "thread" } @@ -104,7 +105,7 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, public init( id: String, variant: Variant, - creationDateTimestamp: TimeInterval = Date().timeIntervalSince1970, + creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), shouldBeVisible: Bool = false, isPinned: Bool = false, messageDraft: String? = nil, diff --git a/SessionMessagingKit/File Server/FileServerAPI.swift b/SessionMessagingKit/File Server/FileServerAPI.swift index 694bf53be..065a8b94c 100644 --- a/SessionMessagingKit/File Server/FileServerAPI.swift +++ b/SessionMessagingKit/File Server/FileServerAPI.swift @@ -15,13 +15,9 @@ public final class FileServerAPI: NSObject { @objc public static let server = "http://filev2.getsession.org" public static let serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" public static let maxFileSize = (10 * 1024 * 1024) // 10 MB - /// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes - /// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP - /// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also - /// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when - /// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only - /// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds. - public static let fileSizeORMultiplier: Double = 2 + + /// Standard timeout is 10 seconds which is a little too short fir file upload/download with slightly larger files + public static let fileTimeout: TimeInterval = 30 // MARK: - File Storage @@ -77,7 +73,7 @@ public final class FileServerAPI: NSObject { return Promise(error: error) } - return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey) + return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey, timeout: FileServerAPI.fileTimeout) .map2 { _, response in guard let response: Data = response else { throw HTTP.Error.parsingFailed } diff --git a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift index 6a1d4fc16..a3588e511 100644 --- a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift @@ -145,7 +145,7 @@ public enum AttachmentDownloadJob: JobExecutor { _ = try attachment .with( state: .downloaded, - creationTimestamp: Date().timeIntervalSince1970, + creationTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), localRelativeFilePath: ( attachment.localRelativeFilePath ?? Attachment.localRelativeFilePath(from: attachment.originalFilePath) diff --git a/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift b/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift index d6897e1ff..d294c27b3 100644 --- a/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift +++ b/SessionMessagingKit/Jobs/Types/DisappearingMessagesJob.swift @@ -3,6 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit +import SessionSnodeKit public enum DisappearingMessagesJob: JobExecutor { public static let maxFailureCount: Int = -1 @@ -17,7 +18,7 @@ public enum DisappearingMessagesJob: JobExecutor { deferred: @escaping (Job) -> () ) { // The 'backgroundTask' gets captured and cleared within the 'completion' block - let timestampNowMs: TimeInterval = ceil(Date().timeIntervalSince1970 * 1000) + let timestampNowMs: TimeInterval = TimeInterval(SnodeAPI.currentOffsetTimestampMs()) var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: #function) let updatedJob: Job? = Storage.shared.write { db in @@ -59,10 +60,14 @@ public extension DisappearingMessagesJob { guard let nextExpirationTimestampMs: Double = nextExpirationTimestampMs else { return nil } + /// The `expiresStartedAtMs` timestamp is now based on the `SnodeAPI.currentOffsetTimestampMs()` value + /// so we need to make sure offset the `nextRunTimestamp` accordingly to ensure it runs at the correct local time + let clockOffsetMs: Int64 = SnodeAPI.clockOffsetMs.wrappedValue + return try? Job .filter(Job.Columns.variant == Job.Variant.disappearingMessages) .fetchOne(db)? - .with(nextRunTimestamp: ceil(nextExpirationTimestampMs / 1000)) + .with(nextRunTimestamp: ceil((nextExpirationTimestampMs - Double(clockOffsetMs)) / 1000)) .saved(db) } diff --git a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift index 6c47e876f..e8d50e5c2 100644 --- a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift @@ -96,7 +96,7 @@ public enum GarbageCollectionJob: JobExecutor { GROUP BY \(interaction[.threadId]) ) AS interactionInfo ON interactionInfo.\(threadIdLiteral) = \(interaction[.threadId]) WHERE ( - \(interaction[.timestampMs]) < \(timestampNow - approxSixMonthsInSeconds) AND + \(interaction[.timestampMs]) < \((timestampNow - approxSixMonthsInSeconds) * 1000) AND interactionInfo.interactionCount >= \(minInteractionsToTrimSql) ) ) diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index bac6dbb5e..1f239920c 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -259,7 +259,10 @@ public extension Message { return try processRawReceivedMessage( db, envelope: envelope, - serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds), + serverExpirationTimestamp: ( + (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) + + ControlMessageProcessRecord.defaultExpirationSeconds + ), serverHash: serverHash, handleClosedGroupKeyUpdateMessages: true ) @@ -275,7 +278,10 @@ public extension Message { let processedMessage: ProcessedMessage? = try processRawReceivedMessage( db, envelope: envelope, - serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds), + serverExpirationTimestamp: ( + (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) + + ControlMessageProcessRecord.defaultExpirationSeconds + ), serverHash: nil, handleClosedGroupKeyUpdateMessages: false ) @@ -407,7 +413,7 @@ public extension Message { let count: Int64 = rawReaction.you ? rawReaction.count - 1 : rawReaction.count - let timestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000))) + let timestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() let maxLength: Int = shouldAddSelfReaction ? 4 : 5 let desiredReactorIds: [String] = reactors .filter { $0 != blindedUserPublicKey && $0 != userPublicKey } // Remove current user for now, will add back if needed diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 051ba7cc2..9b4984627 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -871,6 +871,7 @@ public enum OpenGroupAPI { ], body: bytes ), + timeout: FileServerAPI.fileTimeout, using: dependencies ) .decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) @@ -890,6 +891,7 @@ public enum OpenGroupAPI { server: server, endpoint: .roomFileIndividual(roomToken, fileId) ), + timeout: FileServerAPI.fileTimeout, using: dependencies ) .map { responseInfo, maybeData in @@ -1391,6 +1393,7 @@ public enum OpenGroupAPI { _ db: Database, request: Request, forceBlinded: Bool = false, + timeout: TimeInterval = HTTP.timeout, using dependencies: SMKDependencies = SMKDependencies() ) -> Promise<(OnionRequestResponseInfoType, Data?)> { let urlRequest: URLRequest @@ -1415,6 +1418,6 @@ public enum OpenGroupAPI { return Promise(error: OpenGroupAPIError.signingFailed) } - return dependencies.onionApi.sendOnionRequest(signedRequest, to: request.server, with: publicKey) + return dependencies.onionApi.sendOnionRequest(signedRequest, to: request.server, with: publicKey, timeout: timeout) } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 8dc60c126..9f8f652db 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import WebRTC import SessionUtilitiesKit +import SessionSnodeKit extension MessageReceiver { public static func handleCallMessage(_ db: Database, message: CallMessage) throws { @@ -189,7 +190,7 @@ extension MessageReceiver { body: String(data: messageInfoData, encoding: .utf8), timestampMs: ( message.sentTimestamp.map { Int64($0) } ?? - Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.currentOffsetTimestampMs() ) ) .inserted(db) @@ -235,7 +236,7 @@ extension MessageReceiver { ) let timestampMs: Int64 = ( message.sentTimestamp.map { Int64($0) } ?? - Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.currentOffsetTimestampMs() ) guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 5c5620a81..669e46de0 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import Sodium import SessionUtilitiesKit +import SessionSnodeKit extension MessageReceiver { public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage) throws { @@ -135,7 +136,7 @@ extension MessageReceiver { threadId: groupPublicKey, publicKey: Data(encryptionKeyPair.publicKey), secretKey: Data(encryptionKeyPair.secretKey), - receivedTimestamp: Date().timeIntervalSince1970 + receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) ).insert(db) // Start polling @@ -196,7 +197,7 @@ extension MessageReceiver { threadId: groupPublicKey, publicKey: proto.publicKey.removingIdPrefixIfNeeded(), secretKey: proto.privateKey, - receivedTimestamp: Date().timeIntervalSince1970 + receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) ).insert(db) } catch { @@ -231,7 +232,7 @@ extension MessageReceiver { .infoMessage(db, sender: sender), timestampMs: ( message.sentTimestamp.map { Int64($0) } ?? - Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.currentOffsetTimestampMs() ) ).inserted(db) } @@ -307,7 +308,7 @@ extension MessageReceiver { .infoMessage(db, sender: sender), timestampMs: ( message.sentTimestamp.map { Int64($0) } ?? - Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.currentOffsetTimestampMs() ) ).inserted(db) } @@ -383,7 +384,7 @@ extension MessageReceiver { .infoMessage(db, sender: sender), timestampMs: ( message.sentTimestamp.map { Int64($0) } ?? - Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.currentOffsetTimestampMs() ) ).inserted(db) } @@ -461,7 +462,7 @@ extension MessageReceiver { .infoMessage(db, sender: sender), timestampMs: ( message.sentTimestamp.map { Int64($0) } ?? - Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.currentOffsetTimestampMs() ) ).inserted(db) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift index fd74915a9..a28346f6d 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+DataExtractionNotification.swift @@ -2,6 +2,7 @@ import Foundation import GRDB +import SessionSnodeKit extension MessageReceiver { internal static func handleDataExtractionNotification(_ db: Database, message: DataExtractionNotification) throws { @@ -24,7 +25,7 @@ extension MessageReceiver { }(), timestampMs: ( message.sentTimestamp.map { Int64($0) } ?? - Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.currentOffsetTimestampMs() ) ).inserted(db) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index d16adbe95..06ee54eb3 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import SignalCoreKit import SessionUtilitiesKit +import SessionSnodeKit extension MessageReceiver { internal static func handleMessageRequestResponse( @@ -123,7 +124,7 @@ extension MessageReceiver { variant: .infoMessageRequestAccepted, timestampMs: ( message.sentTimestamp.map { Int64($0) } ?? - Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.currentOffsetTimestampMs() ) ).inserted(db) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index bc8d52a0b..0d219b5b2 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -6,6 +6,7 @@ import Sodium import Curve25519Kit import PromiseKit import SessionUtilitiesKit +import SessionSnodeKit extension MessageSender { public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:]) @@ -24,7 +25,7 @@ extension MessageSender { let membersAsData = members.map { Data(hex: $0) } let admins = [ userPublicKey ] let adminsAsData = admins.map { Data(hex: $0) } - let formationTimestamp: TimeInterval = Date().timeIntervalSince1970 + let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) let thread: SessionThread = try SessionThread .fetchOrCreate(db, id: groupPublicKey, variant: .closedGroup) try ClosedGroup( @@ -91,7 +92,7 @@ extension MessageSender { threadId: groupPublicKey, publicKey: encryptionKeyPair.publicKey, secretKey: encryptionKeyPair.privateKey, - receivedTimestamp: Date().timeIntervalSince1970 + receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) ).insert(db) // Notify the PN server @@ -110,7 +111,7 @@ extension MessageSender { threadId: thread.id, authorId: userPublicKey, variant: .infoClosedGroupCreated, - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) + timestampMs: SnodeAPI.currentOffsetTimestampMs() ).inserted(db) // Start polling @@ -142,7 +143,7 @@ extension MessageSender { threadId: closedGroup.threadId, publicKey: legacyNewKeyPair.publicKey, secretKey: legacyNewKeyPair.privateKey, - receivedTimestamp: Date().timeIntervalSince1970 + receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) ) // Distribute it @@ -230,7 +231,7 @@ extension MessageSender { body: ClosedGroupControlMessage.Kind .nameChange(name: name) .infoMessage(db, sender: userPublicKey), - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) + timestampMs: SnodeAPI.currentOffsetTimestampMs() ).inserted(db) guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } @@ -330,7 +331,7 @@ extension MessageSender { body: ClosedGroupControlMessage.Kind .membersAdded(members: addedMembers.map { Data(hex: $0) }) .infoMessage(db, sender: userPublicKey), - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) + timestampMs: SnodeAPI.currentOffsetTimestampMs() ).inserted(db) guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } @@ -431,7 +432,7 @@ extension MessageSender { body: ClosedGroupControlMessage.Kind .membersRemoved(members: removedMembers.map { Data(hex: $0) }) .infoMessage(db, sender: userPublicKey), - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) + timestampMs: SnodeAPI.currentOffsetTimestampMs() ).inserted(db) guard let newInteractionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } @@ -496,7 +497,7 @@ extension MessageSender { body: ClosedGroupControlMessage.Kind .memberLeft .infoMessage(db, sender: userPublicKey), - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) + timestampMs: SnodeAPI.currentOffsetTimestampMs() ).inserted(db) guard let interactionId: Int64 = interaction.id else { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index f3b6f674c..6c1a3dc40 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -5,6 +5,7 @@ import GRDB import Sodium import SignalCoreKit import SessionUtilitiesKit +import SessionSnodeKit public enum MessageReceiver { private static var lastEncryptionKeyPairRequest: [String: Date] = [:] @@ -144,7 +145,7 @@ public enum MessageReceiver { message.sender = sender message.recipient = userPublicKey message.sentTimestamp = envelope.timestamp - message.receivedTimestamp = UInt64((Date().timeIntervalSince1970) * 1000) + message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) message.groupPublicKey = groupPublicKey message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) } @@ -318,7 +319,7 @@ public enum MessageReceiver { var updatedProfile: Profile = profile // Name - if let name = name, name != profile.name { + if let name = name, !name.isEmpty, name != profile.name { let shouldUpdate: Bool if isCurrentUser { shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) { diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 547ab9521..5cf680554 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -66,8 +66,7 @@ public final class MessageSender { ) throws -> Promise { let (promise, seal) = Promise.pending() let userPublicKey: String = getUserHexEncodedPublicKey(db) - let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) - let messageSendTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) + let messageSendTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs() // Set the timestamp, sender and recipient message.sentTimestamp = ( @@ -202,7 +201,7 @@ public final class MessageSender { recipient: message.recipient!, data: base64EncodedData, ttl: message.ttl, - timestampMs: UInt64(messageSendTimestamp + SnodeAPI.clockOffset.wrappedValue) + timestampMs: UInt64(messageSendTimestamp) ) SnodeAPI @@ -261,6 +260,8 @@ public final class MessageSender { behaviour: .runOnce, details: NotifyPushServerJob.Details(message: snodeMessage) ) + let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]) + .defaulting(to: false) if isMainAppActive { JobRunner.add(db, job: job) @@ -322,7 +323,7 @@ public final class MessageSender { // Set the timestamp, sender and recipient if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set - message.sentTimestamp = UInt64(floor(Date().timeIntervalSince1970 * 1000)) + message.sentTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) } switch destination { @@ -472,7 +473,7 @@ public final class MessageSender { // Set the timestamp, sender and recipient if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set - message.sentTimestamp = UInt64(floor(Date().timeIntervalSince1970 * 1000)) + message.sentTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) } message.sender = userPublicKey @@ -617,7 +618,7 @@ public final class MessageSender { job: DisappearingMessagesJob.updateNextRunIfNeeded( db, interaction: interaction, - startedAtMs: (Date().timeIntervalSince1970 * 1000) + startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs()) ) ) } @@ -636,7 +637,10 @@ public final class MessageSender { } }(), message: message, - serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds) + serverExpirationTimestamp: ( + (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) + + ControlMessageProcessRecord.defaultExpirationSeconds + ) )?.insert(db) // Sync the message if: diff --git a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift index 9e50e80b8..afebe2c10 100644 --- a/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift +++ b/SessionMessagingKit/Sending & Receiving/Typing Indicators/TypingIndicators.swift @@ -3,6 +3,7 @@ import Foundation import GRDB import SessionUtilitiesKit +import SessionSnodeKit public class TypingIndicators { // MARK: - Direction @@ -41,7 +42,7 @@ public class TypingIndicators { self.threadId = threadId self.direction = direction - self.timestampMs = (timestampMs ?? Int64(floor(Date().timeIntervalSince1970 * 1000))) + self.timestampMs = (timestampMs ?? SnodeAPI.currentOffsetTimestampMs()) } fileprivate func start(_ db: Database) { diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 7db42407d..5fb7aaf2a 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -196,7 +196,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView authorId: getUserHexEncodedPublicKey(db), variant: .standardOutgoing, body: body, - timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), + timestampMs: SnodeAPI.currentOffsetTimestampMs(), hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body), expiresInSeconds: try? DisappearingMessagesConfiguration .select(.durationSeconds) diff --git a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift b/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift index 7addb56e5..336a19de8 100644 --- a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift +++ b/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift @@ -93,7 +93,7 @@ public extension SnodeReceivedMessageInfo { return try SnodeReceivedMessageInfo .select(Column.rowID) .filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey, namespace: namespace)) - .filter(SnodeReceivedMessageInfo.Columns.expirationDateMs <= (Date().timeIntervalSince1970 * 1000)) + .filter(SnodeReceivedMessageInfo.Columns.expirationDateMs <= SnodeAPI.currentOffsetTimestampMs()) .asRequest(of: Int64.self) .fetchAll(db) } @@ -122,7 +122,7 @@ public extension SnodeReceivedMessageInfo { SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid == false ) .filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey, namespace: namespace)) - .filter(SnodeReceivedMessageInfo.Columns.expirationDateMs > (Date().timeIntervalSince1970 * 1000)) + .filter(SnodeReceivedMessageInfo.Columns.expirationDateMs > SnodeAPI.currentOffsetTimestampMs()) .order(SnodeReceivedMessageInfo.Columns.id.desc) .fetchOne(db) diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index d2a71b66c..eedb629aa 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -7,13 +7,17 @@ import PromiseKit import SessionUtilitiesKit public protocol OnionRequestAPIType { - static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise - static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> + static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?, timeout: TimeInterval) -> Promise + static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String, timeout: TimeInterval) -> Promise<(OnionRequestResponseInfoType, Data?)> } public extension OnionRequestAPIType { - static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { - sendOnionRequest(request, to: server, using: .v4, with: x25519PublicKey) + static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise { + sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey, timeout: HTTP.timeout) + } + + static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String, timeout: TimeInterval = HTTP.timeout) -> Promise<(OnionRequestResponseInfoType, Data?)> { + sendOnionRequest(request, to: server, using: .v4, with: x25519PublicKey, timeout: timeout) } } @@ -369,7 +373,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { // MARK: - Public API /// Sends an onion request to `snode`. Builds new paths as needed. - public static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise { + public static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise { let payloadJson: JSON = [ "method" : method.rawValue, "params" : parameters ] guard let payload: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []) else { @@ -377,7 +381,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { } /// **Note:** Currently the service nodes only support V3 Onion Requests - return sendOnionRequest(with: payload, to: OnionRequestAPIDestination.snode(snode), version: .v3) + return sendOnionRequest(with: payload, to: OnionRequestAPIDestination.snode(snode), version: .v3, timeout: timeout) .map { _, maybeData in guard let data: Data = maybeData else { throw HTTP.Error.invalidResponse } @@ -393,7 +397,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { } /// Sends an onion request to `server`. Builds new paths as needed. - public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion = .v4, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { + public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion = .v4, with x25519PublicKey: String, timeout: TimeInterval = HTTP.timeout) -> Promise<(OnionRequestResponseInfoType, Data?)> { guard let url = request.url, let host = request.url?.host else { return Promise(error: OnionRequestAPIError.invalidURL) } @@ -412,14 +416,14 @@ public enum OnionRequestAPI: OnionRequestAPIType { scheme: scheme, port: port ) - let promise = sendOnionRequest(with: payload, to: destination, version: version) + let promise = sendOnionRequest(with: payload, to: destination, version: version, timeout: timeout) promise.catch2 { error in SNLog("Couldn't reach server: \(url) due to error: \(error).") } return promise } - public static func sendOnionRequest(with payload: Data, to destination: OnionRequestAPIDestination, version: OnionRequestAPIVersion) -> Promise<(OnionRequestResponseInfoType, Data?)> { + public static func sendOnionRequest(with payload: Data, to destination: OnionRequestAPIDestination, version: OnionRequestAPIVersion, timeout: TimeInterval = HTTP.timeout) -> Promise<(OnionRequestResponseInfoType, Data?)> { let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending() var guardSnode: Snode? @@ -444,7 +448,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { } let destinationSymmetricKey = intermediate.destinationSymmetricKey - HTTP.execute(.post, url, body: body) + HTTP.execute(.post, url, body: body, timeout: timeout) .done2 { responseData in handleResponse( responseData: responseData, @@ -672,7 +676,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { if let timestamp = body["t"] as? Int64 { let offset = timestamp - Int64(floor(Date().timeIntervalSince1970 * 1000)) - SnodeAPI.clockOffset.mutate { $0 = offset } + SnodeAPI.clockOffsetMs.mutate { $0 = offset } } guard 200...299 ~= statusCode else { diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index db0d2903a..eff7c6632 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -19,10 +19,16 @@ public final class SnodeAPI { internal static var snodePool: Atomic> = Atomic([]) /// The offset between the user's clock and the Service Node's clock. Used in cases where the - /// user's clock is incorrect. - /// - /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - public static var clockOffset: Atomic = Atomic(0) + /// user's clock is incorrect + public static var clockOffsetMs: Atomic = Atomic(0) + + public static func currentOffsetTimestampMs() -> Int64 { + return ( + Int64(floor(Date().timeIntervalSince1970 * 1000)) + + SnodeAPI.clockOffsetMs.wrappedValue + ) + } + /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. public static var swarmCache: Atomic<[String: Set]> = Atomic([:]) @@ -546,7 +552,7 @@ public final class SnodeAPI { let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, namespace: namespace, associatedWith: publicKey)?.hash ?? "" // Construct signature - let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue) + let timestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString() let namespaceVerificationString = (namespace == defaultNamespace ? "" : String(namespace)) @@ -647,7 +653,7 @@ public final class SnodeAPI { } // Construct signature - let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue) + let timestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString() guard @@ -1102,3 +1108,11 @@ public final class SnodeAPI { return nil } } + +@objc(SNSnodeAPI) +public final class SNSnodeAPI: NSObject { + @objc(currentOffsetTimestampMs) + public static func currentOffsetTimestampMs() -> UInt64 { + return UInt64(SnodeAPI.currentOffsetTimestampMs()) + } +} diff --git a/SessionUIKit/Components/SessionButton.swift b/SessionUIKit/Components/SessionButton.swift index 6f2c5443b..f7acab456 100644 --- a/SessionUIKit/Components/SessionButton.swift +++ b/SessionUIKit/Components/SessionButton.swift @@ -17,6 +17,25 @@ public final class SessionButton: UIButton { case large } + public struct Info { + public let style: Style + public let title: String + public let isEnabled: Bool + public let onTap: () -> () + + public init( + style: Style, + title: String, + isEnabled: Bool, + onTap: @escaping () -> () + ) { + self.style = style + self.title = title + self.isEnabled = isEnabled + self.onTap = onTap + } + } + private let style: Style public override var isEnabled: Bool { @@ -157,4 +176,10 @@ public final class SessionButton: UIButton { } }() } + + // MARK: - Functions + + public func setStyle(_ style: Style) { + setup(style: style) + } } diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift index 61ed9914e..99ef6bd21 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift @@ -34,6 +34,7 @@ internal enum Theme_ClassicDark: ThemeColors { .messageBubble_outgoingText: .classicDark0, .messageBubble_incomingText: .classicDark6, .messageBubble_overlay: .black_06, + .messageBubble_deliveryStatus: .classicDark5, // MenuButton .menuButton_background: .primary, diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift index d2554fc0d..6e7e7dc32 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift @@ -34,6 +34,7 @@ internal enum Theme_ClassicLight: ThemeColors { .messageBubble_outgoingText: .classicLight0, .messageBubble_incomingText: .classicLight0, .messageBubble_overlay: .black_06, + .messageBubble_deliveryStatus: .classicLight1, // MenuButton .menuButton_background: .primary, diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift index 1775bfed0..e01656c8f 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift @@ -34,6 +34,7 @@ internal enum Theme_OceanDark: ThemeColors { .messageBubble_outgoingText: .oceanDark0, .messageBubble_incomingText: .oceanDark7, .messageBubble_overlay: .black_06, + .messageBubble_deliveryStatus: .oceanDark5, // MenuButton .menuButton_background: .primary, diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift index c22f4fb34..649028a1e 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift @@ -34,6 +34,7 @@ internal enum Theme_OceanLight: ThemeColors { .messageBubble_outgoingText: .oceanLight1, .messageBubble_incomingText: .oceanLight1, .messageBubble_overlay: .black_06, + .messageBubble_deliveryStatus: .oceanLight2, // MenuButton .menuButton_background: .primary, diff --git a/SessionUIKit/Style Guide/Themes/Theme.swift b/SessionUIKit/Style Guide/Themes/Theme.swift index d81a5a3bf..ca0a620d9 100644 --- a/SessionUIKit/Style Guide/Themes/Theme.swift +++ b/SessionUIKit/Style Guide/Themes/Theme.swift @@ -122,6 +122,7 @@ public indirect enum ThemeValue: Hashable { case messageBubble_outgoingText case messageBubble_incomingText case messageBubble_overlay + case messageBubble_deliveryStatus // MenuButton case menuButton_background diff --git a/SessionUtilitiesKit/Database/Models/Identity.swift b/SessionUtilitiesKit/Database/Models/Identity.swift index 9d08731f5..4f3f82ee8 100644 --- a/SessionUtilitiesKit/Database/Models/Identity.swift +++ b/SessionUtilitiesKit/Database/Models/Identity.swift @@ -53,7 +53,7 @@ extension ECKeyPair { public extension Identity { static func generate(from seed: Data) throws -> (ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) { - assert(seed.count == 16) + guard (seed.count == 16) else { throw GeneralError.invalidSeed } let padding = Data(repeating: 0, count: 16) guard diff --git a/SessionUtilitiesKit/General/General.swift b/SessionUtilitiesKit/General/General.swift index 714184ee3..72576fb56 100644 --- a/SessionUtilitiesKit/General/General.swift +++ b/SessionUtilitiesKit/General/General.swift @@ -19,6 +19,7 @@ public enum General { } public enum GeneralError: Error { + case invalidSeed case keyGenerationFailed } diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 065ad3ba1..7d604f0ba 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -111,6 +111,7 @@ public final class JobRunner { fileprivate static var perSessionJobsCompleted: Atomic> = Atomic([]) private static var hasCompletedInitialBecomeActive: Atomic = Atomic(false) private static var shutdownBackgroundTask: Atomic = Atomic(nil) + fileprivate static var canStartQueues: Atomic = Atomic(false) // MARK: - Configuration @@ -161,6 +162,9 @@ public final class JobRunner { queues.wrappedValue[job.variant]?.upsert(job, canStartJob: canStartJob) + // Don't start the queue if the job can't be started + guard canStartJob else { return } + // Start the job runner if needed db.afterNextTransaction { _ in queues.wrappedValue[job.variant]?.start() @@ -188,15 +192,13 @@ public final class JobRunner { queues.wrappedValue[updatedJob.variant]?.insert(updatedJob, before: otherJob) - // Start the job runner if needed - db.afterNextTransaction { _ in - queues.wrappedValue[updatedJob.variant]?.start() - } - return (jobId, updatedJob) } public static func appDidFinishLaunching() { + // Flag that the JobRunner can start it's queues + JobRunner.canStartQueues.mutate { $0 = true } + // Note: 'appDidBecomeActive' will run on first launch anyway so we can // leave those jobs out and can wait until then to start the JobRunner let jobsToRun: (blocking: [Job], nonBlocking: [Job]) = Storage.shared @@ -242,6 +244,9 @@ public final class JobRunner { } public static func appDidBecomeActive() { + // Flag that the JobRunner can start it's queues + JobRunner.canStartQueues.mutate { $0 = true } + // If we have a running "sutdownBackgroundTask" then we want to cancel it as otherwise it // can result in the database being suspended and us being unable to interact with it at all shutdownBackgroundTask.mutate { @@ -291,6 +296,11 @@ public final class JobRunner { exceptForVariant: Job.Variant? = nil, onComplete: (() -> ())? = nil ) { + // Inform the JobRunner that it can't start any queues (this is to prevent queues from + // rescheduling themselves while in the background, when the app restarts or becomes active + // the JobRunenr will update this flag) + JobRunner.canStartQueues.mutate { $0 = false } + // Stop all queues except for the one containing the `exceptForVariant` queues.wrappedValue .values @@ -632,6 +642,7 @@ private final class JobQueue { fileprivate func start(force: Bool = false) { // We only want the JobRunner to run in the main app guard CurrentAppContext().isMainApp else { return } + guard JobRunner.canStartQueues.wrappedValue else { return } guard force || !isRunning.wrappedValue else { return } // The JobRunner runs synchronously we need to ensure this doesn't start @@ -852,8 +863,9 @@ private final class JobQueue { .fetchOne(db) } - // If there are no remaining jobs the trigger the 'onQueueDrained' callback and stop - guard let nextJobTimestamp: TimeInterval = nextJobTimestamp else { + // If there are no remaining jobs or the JobRunner isn't allowed to start any queues then trigger + // the 'onQueueDrained' callback and stop + guard let nextJobTimestamp: TimeInterval = nextJobTimestamp, JobRunner.canStartQueues.wrappedValue else { if executionType != .concurrent || jobsCurrentlyRunning.wrappedValue.isEmpty { self.onQueueDrained?() } @@ -1064,6 +1076,8 @@ private final class JobQueue { queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) } } } + + performCleanUp(for: job, result: .failed) return } @@ -1082,7 +1096,7 @@ private final class JobQueue { try job.dependantJobs .updateAll( db, - Job.Columns.failureCount.set(to: job.failureCount), + Job.Columns.failureCount.set(to: (job.failureCount + 1)), Job.Columns.nextRunTimestamp.set(to: (nextRunTimestamp + (1 / 1000))) ) diff --git a/SignalUtilitiesKit/Configuration.swift b/SignalUtilitiesKit/Configuration.swift index 2c0414db2..4ab7e2d50 100644 --- a/SignalUtilitiesKit/Configuration.swift +++ b/SignalUtilitiesKit/Configuration.swift @@ -8,10 +8,7 @@ import SessionMessagingKit public enum Configuration { public static func performMainSetup() { // Need to do this first to ensure the legacy database exists - SNUtilitiesKit.configure( - maxFileSize: UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier) - ) - + SNUtilitiesKit.configure(maxFileSize: UInt(FileServerAPI.maxFileSize)) SNMessagingKit.configure() SNSnodeKit.configure() SNUIKit.configure()