Merge remote-tracking branch 'upstream/dev' into feature/updated-user-config-handling
# Conflicts: # Session/Media Viewing & Editing/PhotoCapture.swift # Session/Meta/Translations/de.lproj/Localizable.strings # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/es.lproj/Localizable.strings # Session/Meta/Translations/fa.lproj/Localizable.strings # Session/Meta/Translations/fi.lproj/Localizable.strings # Session/Meta/Translations/fr.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/hr.lproj/Localizable.strings # Session/Meta/Translations/id-ID.lproj/Localizable.strings # Session/Meta/Translations/it.lproj/Localizable.strings # Session/Meta/Translations/ja.lproj/Localizable.strings # Session/Meta/Translations/nl.lproj/Localizable.strings # Session/Meta/Translations/pl.lproj/Localizable.strings # Session/Meta/Translations/pt_BR.lproj/Localizable.strings # Session/Meta/Translations/ru.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/sk.lproj/Localizable.strings # Session/Meta/Translations/sv.lproj/Localizable.strings # Session/Meta/Translations/th.lproj/Localizable.strings # Session/Meta/Translations/vi-VN.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Meta/Translations/zh_CN.lproj/Localizable.strings # Session/Notifications/AppNotifications.swift # Session/Onboarding/RestoreVC.swift # Session/Shared/SessionTableViewController.swift # Session/Shared/SessionTableViewModel.swift # SessionMessagingKit/Calls/WebRTCSession.swift # SessionMessagingKit/Database/Models/Attachment.swift # SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionSnodeKit/SnodeAPI.swift # SessionUtilitiesKit/Database/Models/Identity.swift # SessionUtilitiesKit/JobRunner/JobRunner.swift
This commit is contained in:
commit
4f8fb63f2c
|
@ -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,
|
||||
|
|
|
@ -8,15 +8,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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,10 +278,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() {
|
||||
|
|
|
@ -407,7 +407,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
|
||||
|
||||
|
@ -533,7 +533,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(
|
||||
|
@ -652,7 +652,7 @@ extension ConversationVC:
|
|||
threadVariant: threadVariant,
|
||||
threadIsMessageRequest: threadIsMessageRequest,
|
||||
direction: .outgoing,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
|
||||
if needsToStartTypingIndicator {
|
||||
|
@ -1237,7 +1237,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
|
||||
|
@ -2092,7 +2092,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
|
||||
|
@ -2337,7 +2337,7 @@ extension ConversationVC {
|
|||
for: self.viewModel.threadData.threadId,
|
||||
threadVariant: self.viewModel.threadData.threadVariant,
|
||||
isNewThread: false,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
@ -391,13 +415,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 ||
|
||||
(
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#import <SignalCoreKit/OWSAsserts.h>
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
|
||||
#import <SessionSnodeKit/SessionSnodeKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -76,7 +77,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;
|
||||
|
|
|
@ -162,7 +162,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
|||
authorId: getUserHexEncodedPublicKey(db),
|
||||
variant: .infoDisappearingMessagesUpdate,
|
||||
body: config.messageInfoString(with: nil),
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
.inserted(db)
|
||||
|
||||
|
|
|
@ -718,7 +718,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
threadId: thread.id,
|
||||
authorId: userId,
|
||||
variant: .standardOutgoing,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
|
||||
expiresInSeconds: try? DisappearingMessagesConfiguration
|
||||
.select(.durationSeconds)
|
||||
.filter(id: userId)
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import Combine
|
||||
import AVFoundation
|
||||
import CoreServices
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
protocol PhotoCaptureDelegate: AnyObject {
|
||||
|
@ -516,6 +517,9 @@ class CaptureOutput {
|
|||
// leaving it enabled causes all audio to be lost on videos longer
|
||||
// than the default length (10s).
|
||||
movieOutput.movieFragmentInterval = CMTime.invalid
|
||||
|
||||
// Ensure the recorded movie can't go over the maximum file server size
|
||||
movieOutput.maxRecordedFileSize = Int64(FileServerAPI.maxFileSize)
|
||||
}
|
||||
|
||||
var photoOutput: AVCaptureOutput? {
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "خوانده شد";
|
||||
"MESSAGE_STATE_SENT" = "ارسال شد";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -596,5 +596,9 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
"REMOVE_AVATAR" = "Remove";
|
||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||
|
|
|
@ -537,7 +537,7 @@ class NotificationActionHandler {
|
|||
authorId: getUserHexEncodedPublicKey(db),
|
||||
variant: .standardOutgoing,
|
||||
body: replyText,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
|
||||
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText),
|
||||
expiresInSeconds: try? DisappearingMessagesConfiguration
|
||||
.select(.durationSeconds)
|
||||
|
|
|
@ -104,7 +104,6 @@ public final class FullConversationCell: UITableViewCell {
|
|||
let result: UIImageView = UIImageView()
|
||||
result.clipsToBounds = true
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.layer.cornerRadius = (FullConversationCell.statusIndicatorSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
|
|
@ -41,6 +41,14 @@ class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection,
|
|||
|
||||
open var title: String { preconditionFailure("abstract class - override in subclass") }
|
||||
open var emptyStateTextPublisher: AnyPublisher<String?, Never> { Just(nil).eraseToAnyPublisher() }
|
||||
open var settingsData: [SectionModel] { preconditionFailure("abstract class - override in subclass") }
|
||||
open var observableSettingsData: ObservableData {
|
||||
preconditionFailure("abstract class - override in subclass")
|
||||
}
|
||||
open var footerView: AnyPublisher<UIView?, Never> { Just(nil).eraseToAnyPublisher() }
|
||||
open var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> {
|
||||
Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
fileprivate var hasEmittedInitialData: Bool = false
|
||||
public private(set) var tableData: [SectionModel] = []
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
import GRDB
|
||||
import WebRTC
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public protocol WebRTCSessionDelegate: AnyObject {
|
||||
var videoCapturer: RTCVideoCapturer { get }
|
||||
|
@ -165,7 +166,6 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
return Future<Void, Error> { [weak self] resolver in
|
||||
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
|
||||
if let error = error {
|
||||
resolver(Result.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -190,7 +190,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
uuid: uuid,
|
||||
kind: .offer,
|
||||
sdps: [ sdp.sdp ],
|
||||
sentTimestampMs: UInt64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
),
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
interactionId: nil
|
||||
|
|
|
@ -1291,7 +1291,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,
|
||||
|
@ -1662,7 +1662,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
state: .invalid,
|
||||
contentType: "",
|
||||
byteCount: 0,
|
||||
creationTimestamp: Date().timeIntervalSince1970,
|
||||
creationTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000),
|
||||
sourceFilename: nil,
|
||||
downloadUrl: nil,
|
||||
localRelativeFilePath: nil,
|
||||
|
|
|
@ -7,6 +7,7 @@ import Combine
|
|||
import GRDB
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "attachment" }
|
||||
|
@ -1143,7 +1144,7 @@ extension Attachment {
|
|||
state: .uploaded,
|
||||
creationTimestamp: (
|
||||
self.creationTimestamp ??
|
||||
Date().timeIntervalSince1970
|
||||
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
),
|
||||
downloadUrl: fileId.map { "\(FileServerAPI.server)/file/\($0)" },
|
||||
encryptionKey: encryptionKey,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
import GRDB
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "linkPreview" }
|
||||
|
@ -59,7 +60,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?,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -15,15 +15,8 @@ public enum FileServerAPI {
|
|||
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
|
||||
|
||||
|
@ -84,7 +77,7 @@ public enum FileServerAPI {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey)
|
||||
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey, timeout: FileServerAPI.fileTimeout)
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.flatMap { _, response -> AnyPublisher<Data, Error> in
|
||||
guard let response: Data = response else {
|
||||
|
|
|
@ -174,7 +174,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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,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)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -252,7 +252,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
|
||||
)
|
||||
|
@ -268,7 +271,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
|
||||
)
|
||||
|
@ -400,7 +406,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
|
||||
|
|
|
@ -932,6 +932,7 @@ public enum OpenGroupAPI {
|
|||
],
|
||||
body: bytes
|
||||
),
|
||||
timeout: FileServerAPI.fileTimeout,
|
||||
using: dependencies
|
||||
)
|
||||
.decoded(as: FileUploadResponse.self, using: dependencies)
|
||||
|
@ -953,6 +954,7 @@ public enum OpenGroupAPI {
|
|||
server: server,
|
||||
endpoint: .roomFileIndividual(roomToken, fileId)
|
||||
),
|
||||
timeout: FileServerAPI.fileTimeout,
|
||||
using: dependencies
|
||||
)
|
||||
.flatMap { responseInfo, maybeData -> AnyPublisher<(ResponseInfoType, Data), Error> in
|
||||
|
@ -1489,6 +1491,7 @@ public enum OpenGroupAPI {
|
|||
_ db: Database,
|
||||
request: Request<T, Endpoint>,
|
||||
forceBlinded: Bool = false,
|
||||
timeout: TimeInterval = HTTP.defaultTimeout,
|
||||
using dependencies: SMKDependencies = SMKDependencies(
|
||||
queue: OpenGroupAPI.workQueue
|
||||
)
|
||||
|
@ -1524,7 +1527,7 @@ public enum OpenGroupAPI {
|
|||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.subscribe(on: dependencies.queue)
|
||||
.flatMap { dependencies.onionApi.sendOnionRequest(signedRequest, to: request.server, with: publicKey) }
|
||||
.flatMap { dependencies.onionApi.sendOnionRequest(signedRequest, to: request.server, with: publicKey, timeout: timeout) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
@ -237,7 +238,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 }
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
import GRDB
|
||||
import Sodium
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageReceiver {
|
||||
public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage) throws {
|
||||
|
@ -136,7 +137,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
|
||||
|
@ -197,7 +198,7 @@ extension MessageReceiver {
|
|||
threadId: groupPublicKey,
|
||||
publicKey: proto.publicKey.removingIdPrefixIfNeeded(),
|
||||
secretKey: proto.privateKey,
|
||||
receivedTimestamp: Date().timeIntervalSince1970
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
).insert(db)
|
||||
}
|
||||
catch {
|
||||
|
@ -232,7 +233,7 @@ extension MessageReceiver {
|
|||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
@ -308,7 +309,7 @@ extension MessageReceiver {
|
|||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
@ -384,7 +385,7 @@ extension MessageReceiver {
|
|||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
@ -462,7 +463,7 @@ extension MessageReceiver {
|
|||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import Combine
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageReceiver {
|
||||
internal static func handleMessageRequestResponse(
|
||||
|
@ -128,7 +129,7 @@ extension MessageReceiver {
|
|||
variant: .infoMessageRequestAccepted,
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import GRDB
|
|||
import Sodium
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageSender {
|
||||
public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:])
|
||||
|
@ -32,7 +33,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
|
||||
let memberSendData: [MessageSender.PreparedSendData]
|
||||
|
||||
|
@ -51,7 +52,7 @@ extension MessageSender {
|
|||
threadId: groupPublicKey,
|
||||
publicKey: encryptionKeyPair.publicKey,
|
||||
secretKey: encryptionKeyPair.privateKey,
|
||||
receivedTimestamp: Date().timeIntervalSince1970
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
).insert(db)
|
||||
|
||||
// Create the member objects
|
||||
|
@ -271,7 +272,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 }
|
||||
|
@ -380,7 +381,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 }
|
||||
|
@ -481,7 +482,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 }
|
||||
|
@ -554,7 +555,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 {
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import GRDB
|
||||
import Sodium
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public enum MessageReceiver {
|
||||
private static var lastEncryptionKeyPairRequest: [String: Date] = [:]
|
||||
|
@ -143,7 +144,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) }
|
||||
|
||||
|
|
|
@ -145,11 +145,12 @@ public final class MessageSender {
|
|||
message: Message,
|
||||
to destination: Message.Destination,
|
||||
interactionId: Int64?,
|
||||
isSyncMessage: Bool = false,
|
||||
using dependencies: SMKDependencies = SMKDependencies()
|
||||
) throws -> PreparedSendData {
|
||||
// Common logic for all destinations
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
|
||||
let messageSendTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
let messageSendTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
let updatedMessage: Message = message
|
||||
|
||||
// Set the message 'sentTimestamp' (Visible messages will already have their sent timestamp set)
|
||||
|
@ -354,7 +355,7 @@ public final class MessageSender {
|
|||
recipient: message.recipient!,
|
||||
data: base64EncodedData,
|
||||
ttl: message.ttl,
|
||||
timestampMs: UInt64(messageSendTimestamp + SnodeAPI.clockOffset.wrappedValue)
|
||||
timestampMs: UInt64(messageSendTimestamp)
|
||||
)
|
||||
|
||||
return PreparedSendData(
|
||||
|
@ -931,7 +932,7 @@ public final class MessageSender {
|
|||
job: DisappearingMessagesJob.updateNextRunIfNeeded(
|
||||
db,
|
||||
interaction: interaction,
|
||||
startedAtMs: (Date().timeIntervalSince1970 * 1000)
|
||||
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -950,7 +951,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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -200,7 +200,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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -7,8 +7,18 @@ import GRDB
|
|||
import SessionUtilitiesKit
|
||||
|
||||
public protocol OnionRequestAPIType {
|
||||
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> AnyPublisher<(ResponseInfoType, Data?), Error>
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> AnyPublisher<(ResponseInfoType, Data?), Error>
|
||||
static func sendOnionRequest(_ payload: Data, to snode: Snode, timeout: TimeInterval) -> AnyPublisher<(ResponseInfoType, Data?), Error>
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String, timeout: TimeInterval) -> AnyPublisher<(ResponseInfoType, Data?), Error>
|
||||
}
|
||||
|
||||
public extension OnionRequestAPIType {
|
||||
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
|
||||
return sendOnionRequest(payload, to: snode, timeout: HTTP.defaultTimeout)
|
||||
}
|
||||
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
|
||||
return sendOnionRequest(request, to: server, with: x25519PublicKey, timeout: HTTP.defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
||||
|
@ -409,13 +419,15 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
/// Sends an onion request to `snode`. Builds new paths as needed.
|
||||
public static func sendOnionRequest(
|
||||
_ payload: Data,
|
||||
to snode: Snode
|
||||
to snode: Snode,
|
||||
timeout: TimeInterval = HTTP.defaultTimeout
|
||||
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
|
||||
/// **Note:** Currently the service nodes only support V3 Onion Requests
|
||||
return sendOnionRequest(
|
||||
with: payload,
|
||||
to: OnionRequestAPIDestination.snode(snode),
|
||||
version: .v3
|
||||
version: .v3,
|
||||
timeout: timeout
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -423,7 +435,8 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
public static func sendOnionRequest(
|
||||
_ request: URLRequest,
|
||||
to server: String,
|
||||
with x25519PublicKey: String
|
||||
with x25519PublicKey: String,
|
||||
timeout: TimeInterval = HTTP.defaultTimeout
|
||||
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
|
||||
guard let url = request.url, let host = request.url?.host else {
|
||||
return Fail(error: OnionRequestAPIError.invalidURL)
|
||||
|
@ -448,7 +461,8 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
scheme: scheme,
|
||||
port: port
|
||||
),
|
||||
version: .v4
|
||||
version: .v4,
|
||||
timeout: timeout
|
||||
)
|
||||
.handleEvents(
|
||||
receiveCompletion: { result in
|
||||
|
@ -465,7 +479,8 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
public static func sendOnionRequest(
|
||||
with payload: Data,
|
||||
to destination: OnionRequestAPIDestination,
|
||||
version: OnionRequestAPIVersion
|
||||
version: OnionRequestAPIVersion,
|
||||
timeout: TimeInterval = HTTP.defaultTimeout
|
||||
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
|
||||
var guardSnode: Snode?
|
||||
|
||||
|
@ -486,7 +501,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
|
||||
// TODO: Replace 'json' with a codable typed
|
||||
return encode(ciphertext: onion, json: parameters)
|
||||
.flatMap { body in HTTP.execute(.post, url, body: body) }
|
||||
.flatMap { body in HTTP.execute(.post, url, body: body, timeout: timeout) }
|
||||
.flatMap { responseData in
|
||||
handleResponse(
|
||||
responseData: responseData,
|
||||
|
@ -686,7 +701,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 {
|
||||
|
|
|
@ -24,7 +24,7 @@ public final class SnodeAPI {
|
|||
/// user's clock is incorrect.
|
||||
///
|
||||
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||
public static var clockOffset: Atomic<Int64> = Atomic(0)
|
||||
public static var clockOffsetMs: Atomic<Int64> = Atomic(0)
|
||||
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||
public static var swarmCache: Atomic<[String: Set<Snode>]> = Atomic([:])
|
||||
|
||||
|
@ -51,10 +51,10 @@ public final class SnodeAPI {
|
|||
private static let snodeFailureThreshold: Int = 3
|
||||
private static let minSnodePoolCount: Int = 12
|
||||
|
||||
private static func offsetTimestampMsNow() -> UInt64 {
|
||||
return UInt64(
|
||||
public static func currentOffsetTimestampMs() -> Int64 {
|
||||
return Int64(
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000)) +
|
||||
SnodeAPI.clockOffset.wrappedValue
|
||||
SnodeAPI.clockOffsetMs.wrappedValue
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -388,7 +388,7 @@ public final class SnodeAPI {
|
|||
namespace: namespace,
|
||||
pubkey: targetPublicKey,
|
||||
subkey: nil,
|
||||
timestampMs: SnodeAPI.offsetTimestampMsNow(),
|
||||
timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
|
||||
ed25519PublicKey: keyPair.publicKey,
|
||||
ed25519SecretKey: keyPair.secretKey
|
||||
)
|
||||
|
@ -494,7 +494,7 @@ public final class SnodeAPI {
|
|||
message: message,
|
||||
namespace: namespace,
|
||||
subkey: nil,
|
||||
timestampMs: SnodeAPI.offsetTimestampMsNow(),
|
||||
timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
|
||||
ed25519PublicKey: userED25519KeyPair.publicKey,
|
||||
ed25519SecretKey: userED25519KeyPair.secretKey
|
||||
)
|
||||
|
@ -569,7 +569,7 @@ public final class SnodeAPI {
|
|||
message: message,
|
||||
namespace: namespace,
|
||||
subkey: nil, // TODO: Need to get this
|
||||
timestampMs: SnodeAPI.offsetTimestampMsNow(),
|
||||
timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
|
||||
ed25519PublicKey: userED25519KeyPair.publicKey,
|
||||
ed25519SecretKey: userED25519KeyPair.secretKey
|
||||
)
|
||||
|
@ -1301,3 +1301,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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -42,7 +42,8 @@ public struct Identity: Codable, Identifiable, FetchableRecord, PersistableRecor
|
|||
|
||||
public extension Identity {
|
||||
static func generate(from seed: Data) throws -> (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) {
|
||||
assert(seed.count == 16)
|
||||
guard (seed.count == 16) else { throw GeneralError.invalidSeed }
|
||||
|
||||
let padding = Data(repeating: 0, count: 16)
|
||||
|
||||
guard
|
||||
|
|
|
@ -18,6 +18,7 @@ public enum General {
|
|||
}
|
||||
|
||||
public enum GeneralError: Error {
|
||||
case invalidSeed
|
||||
case keyGenerationFailed
|
||||
case randomGenerationFailed
|
||||
}
|
||||
|
|
|
@ -111,6 +111,7 @@ public final class JobRunner {
|
|||
fileprivate static var perSessionJobsCompleted: Atomic<Set<Int64>> = Atomic([])
|
||||
private static var hasCompletedInitialBecomeActive: Atomic<Bool> = Atomic(false)
|
||||
private static var shutdownBackgroundTask: Atomic<OWSBackgroundTask?> = Atomic(nil)
|
||||
fileprivate static var canStartQueues: Atomic<Bool> = 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
|
||||
|
@ -634,7 +644,8 @@ private final class JobQueue {
|
|||
guard
|
||||
HasAppContext() &&
|
||||
CurrentAppContext().isMainApp &&
|
||||
!CurrentAppContext().isRunningTests
|
||||
!CurrentAppContext().isRunningTests &&
|
||||
JobRunner.canStartQueues.wrappedValue
|
||||
else { return }
|
||||
guard force || !isRunning.wrappedValue else { return }
|
||||
|
||||
|
@ -856,8 +867,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?()
|
||||
}
|
||||
|
@ -1068,6 +1080,8 @@ private final class JobQueue {
|
|||
queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) }
|
||||
}
|
||||
}
|
||||
|
||||
performCleanUp(for: job, result: .failed)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1086,7 +1100,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)))
|
||||
)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue