Merge branch 'dev' into accessibility_ids_3

This commit is contained in:
Emily 2023-02-02 11:41:48 +11:00
commit ec0b35a9a2
73 changed files with 438 additions and 124 deletions

View File

@ -7173,7 +7173,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 389; CURRENT_PROJECT_VERSION = 390;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -7212,7 +7212,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 2.2.4; MARKETING_VERSION = 2.2.5;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7245,7 +7245,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 389; CURRENT_PROJECT_VERSION = 390;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -7284,7 +7284,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 2.2.4; MARKETING_VERSION = 2.2.5;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session; PRODUCT_NAME = Session;

View File

@ -206,7 +206,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId)
else { return } else { return }
let timestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) let timestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
let message: CallMessage = CallMessage( let message: CallMessage = CallMessage(
uuid: self.uuid, uuid: self.uuid,
kind: .preOffer, kind: .preOffer,

View File

@ -9,15 +9,17 @@ import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
private protocol TableViewTouchDelegate { private protocol TableViewTouchDelegate {
func tableViewWasTouched(_ tableView: TableView) func tableViewWasTouched(_ tableView: TableView, withView hitView: UIView?)
} }
private final class TableView: UITableView { private final class TableView: UITableView {
var touchDelegate: TableViewTouchDelegate? var touchDelegate: TableViewTouchDelegate?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
touchDelegate?.tableViewWasTouched(self) let resultingView: UIView? = super.hitTest(point, with: event)
return 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 { if nameTextField.isFirstResponder {
nameTextField.resignFirstResponder() 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() { @objc private func close() {

View File

@ -409,7 +409,7 @@ extension ConversationVC:
// flags appropriately // flags appropriately
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true) 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 linkPreviewDraft: LinkPreviewDraft? = snInputView.linkPreviewInfo?.draft
let quoteModel: QuotedReplyModel? = snInputView.quoteDraftInfo?.model let quoteModel: QuotedReplyModel? = snInputView.quoteDraftInfo?.model
@ -534,7 +534,7 @@ extension ConversationVC:
// flags appropriately // flags appropriately
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true) 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 // If this was a message request then approve it
approveMessageRequestIfNeeded( approveMessageRequestIfNeeded(
@ -640,7 +640,7 @@ extension ConversationVC:
threadVariant: threadVariant, threadVariant: threadVariant,
threadIsMessageRequest: threadIsMessageRequest, threadIsMessageRequest: threadIsMessageRequest,
direction: .outgoing, direction: .outgoing,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
) )
if needsToStartTypingIndicator { if needsToStartTypingIndicator {
@ -1219,7 +1219,7 @@ extension ConversationVC:
guard !threadIsMessageRequest else { return } guard !threadIsMessageRequest else { return }
// Perform local rate limiting (don't allow more than 20 reactions within 60 seconds) // 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 let recentReactionTimestamps: [Int64] = General.cache.wrappedValue.recentReactionTimestamps
guard guard
@ -2049,7 +2049,7 @@ extension ConversationVC:
// Create URL // Create URL
let directory: String = OWSTemporaryDirectory() 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) let url: URL = URL(fileURLWithPath: directory).appendingPathComponent(fileName)
// Set up audio session // Set up audio session
@ -2290,7 +2290,7 @@ extension ConversationVC {
for: self.viewModel.threadData.threadId, for: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant, threadVariant: self.viewModel.threadData.threadVariant,
isNewThread: false, isNewThread: false,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
) )
} }

View File

@ -142,18 +142,33 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
private lazy var reactionContainerView = ReactionContainerView() 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 = { internal lazy var messageStatusImageView: UIImageView = {
let result = UIImageView() let result = UIImageView()
result.accessibilityLabel = "Message sent status tick" result.accessibilityLabel = "Message sent status tick"
result.contentMode = .scaleAspectFit result.contentMode = .scaleAspectFit
result.layer.cornerRadius = VisibleMessageCell.messageStatusImageViewSize / 2 result.themeTintColor = .messageBubble_deliveryStatus
result.layer.masksToBounds = true
return result return result
}() }()
// MARK: - Settings // MARK: - Settings
private static let messageStatusImageViewSize: CGFloat = 16 private static let messageStatusImageViewSize: CGFloat = 12
private static let authorLabelBottomSpacing: CGFloat = 4 private static let authorLabelBottomSpacing: CGFloat = 4
private static let groupThreadHSpacing: CGFloat = 12 private static let groupThreadHSpacing: CGFloat = 12
private static let profilePictureSize = Values.verySmallProfilePictureSize private static let profilePictureSize = Values.verySmallProfilePictureSize
@ -236,13 +251,22 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
underBubbleStackView.pin(.bottom, to: .bottom, of: self) underBubbleStackView.pin(.bottom, to: .bottom, of: self)
underBubbleStackView.addArrangedSubview(reactionContainerView) underBubbleStackView.addArrangedSubview(reactionContainerView)
underBubbleStackView.addArrangedSubview(messageStatusImageView) underBubbleStackView.addArrangedSubview(messageStatusContainerView)
messageStatusContainerView.addSubview(messageStatusLabel)
messageStatusContainerView.addSubview(messageStatusImageView)
reactionContainerView.widthAnchor reactionContainerView.widthAnchor
.constraint(lessThanOrEqualTo: underBubbleStackView.widthAnchor) .constraint(lessThanOrEqualTo: underBubbleStackView.widthAnchor)
.isActive = true .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(.width, to: VisibleMessageCell.messageStatusImageViewSize)
messageStatusImageView.set(.height, 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() { override func setUpGestureRecognizers() {
@ -389,13 +413,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
) )
// Message status image view // Message status image view
let (image, tintColor) = cellViewModel.state.statusIconInfo( let (image, statusText, tintColor) = cellViewModel.state.statusIconInfo(
variant: cellViewModel.variant, variant: cellViewModel.variant,
hasAtLeastOneReadReceipt: cellViewModel.hasAtLeastOneReadReceipt hasAtLeastOneReadReceipt: cellViewModel.hasAtLeastOneReadReceipt
) )
messageStatusLabel.text = statusText
messageStatusLabel.themeTextColor = tintColor
messageStatusImageView.image = image messageStatusImageView.image = image
messageStatusImageView.themeTintColor = tintColor messageStatusImageView.themeTintColor = tintColor
messageStatusImageView.isHidden = ( messageStatusContainerView.isHidden = (
cellViewModel.variant != .standardOutgoing || cellViewModel.variant != .standardOutgoing ||
cellViewModel.variant == .infoCall || cellViewModel.variant == .infoCall ||
( (

View File

@ -8,6 +8,7 @@
#import <QuartzCore/QuartzCore.h> #import <QuartzCore/QuartzCore.h>
#import <SignalCoreKit/NSDate+OWS.h> #import <SignalCoreKit/NSDate+OWS.h>
#import <SessionUtilitiesKit/NSTimer+Proxying.h> #import <SessionUtilitiesKit/NSTimer+Proxying.h>
#import <SessionSnodeKit/SessionSnodeKit.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -75,7 +76,7 @@ const CGFloat kDisappearingMessageIconSize = 12.f;
return; return;
} }
uint64_t nowTimestamp = [NSDate ows_millisecondTimeStamp]; uint64_t nowTimestamp = [SNSnodeAPI currentOffsetTimestampMs];
CGFloat secondsLeft CGFloat secondsLeft
= (self.expirationTimestamp > nowTimestamp ? (self.expirationTimestamp - nowTimestamp) / 1000.f : 0.f); = (self.expirationTimestamp > nowTimestamp ? (self.expirationTimestamp - nowTimestamp) / 1000.f : 0.f);
CGFloat progress = 0.f; CGFloat progress = 0.f;

View File

@ -168,7 +168,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
authorId: getUserHexEncodedPublicKey(db), authorId: getUserHexEncodedPublicKey(db),
variant: .infoDisappearingMessagesUpdate, variant: .infoDisappearingMessagesUpdate,
body: config.messageInfoString(with: nil), body: config.messageInfoString(with: nil),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
) )
.inserted(db) .inserted(db)

View File

@ -622,7 +622,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
threadId: thread.id, threadId: thread.id,
authorId: userId, authorId: userId,
variant: .standardOutgoing, variant: .standardOutgoing,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), timestampMs: SnodeAPI.currentOffsetTimestampMs(),
expiresInSeconds: try? DisappearingMessagesConfiguration expiresInSeconds: try? DisappearingMessagesConfiguration
.select(.durationSeconds) .select(.durationSeconds)
.filter(id: userId) .filter(id: userId)

View File

@ -6,6 +6,7 @@ import Foundation
import AVFoundation import AVFoundation
import PromiseKit import PromiseKit
import CoreServices import CoreServices
import SessionMessagingKit
protocol PhotoCaptureDelegate: AnyObject { protocol PhotoCaptureDelegate: AnyObject {
func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment) func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment)
@ -463,6 +464,9 @@ class CaptureOutput {
// leaving it enabled causes all audio to be lost on videos longer // leaving it enabled causes all audio to be lost on videos longer
// than the default length (10s). // than the default length (10s).
movieOutput.movieFragmentInterval = CMTime.invalid 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? { var photoOutput: AVCaptureOutput? {

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "خوانده شد"; "MESSAGE_STATE_READ" = "خوانده شد";
"MESSAGE_STATE_SENT" = "ارسال شد"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -596,3 +596,7 @@
"MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_READ" = "Read";
"MESSAGE_STATE_SENT" = "Sent"; "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_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";

View File

@ -532,7 +532,7 @@ class NotificationActionHandler {
authorId: getUserHexEncodedPublicKey(db), authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing, variant: .standardOutgoing,
body: replyText, body: replyText,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), timestampMs: SnodeAPI.currentOffsetTimestampMs(),
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText), hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText),
expiresInSeconds: try? DisappearingMessagesConfiguration expiresInSeconds: try? DisappearingMessagesConfiguration
.select(.durationSeconds) .select(.durationSeconds)

View File

@ -198,7 +198,7 @@ final class RestoreVC: BaseVC {
do { do {
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic) let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
let seed = Data(hex: hexEncodedSeed) let seed = Data(hex: hexEncodedSeed)
let (ed25519KeyPair, x25519KeyPair) = try! Identity.generate(from: seed) let (ed25519KeyPair, x25519KeyPair) = try Identity.generate(from: seed)
Onboarding.Flow.recover.preregister(with: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair) Onboarding.Flow.recover.preregister(with: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
mnemonicTextView.resignFirstResponder() mnemonicTextView.resignFirstResponder()

View File

@ -104,7 +104,6 @@ public final class FullConversationCell: UITableViewCell {
let result: UIImageView = UIImageView() let result: UIImageView = UIImageView()
result.clipsToBounds = true result.clipsToBounds = true
result.contentMode = .scaleAspectFit result.contentMode = .scaleAspectFit
result.layer.cornerRadius = (FullConversationCell.statusIndicatorSize / 2)
return result return result
}() }()

View File

@ -20,6 +20,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
private var dataStreamJustFailed: Bool = false private var dataStreamJustFailed: Bool = false
private var dataChangeCancellable: AnyCancellable? private var dataChangeCancellable: AnyCancellable?
private var disposables: Set<AnyCancellable> = Set() private var disposables: Set<AnyCancellable> = Set()
private var onFooterTap: (() -> ())?
public var viewModelType: AnyObject.Type { return type(of: viewModel) } public var viewModelType: AnyObject.Type { return type(of: viewModel) }
@ -45,6 +46,30 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
return result return result
}() }()
private lazy var fadeView: GradientView = {
let result: GradientView = GradientView()
result.themeBackgroundGradient = [
.value(.backgroundPrimary, alpha: 0), // Want this to take up 20% (~25pt)
.backgroundPrimary,
.backgroundPrimary,
.backgroundPrimary,
.backgroundPrimary
]
result.set(.height, to: Values.footerGradientHeight(window: UIApplication.shared.keyWindow))
result.isHidden = true
return result
}()
private lazy var footerButton: SessionButton = {
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
result.translatesAutoresizingMaskIntoConstraints = false
result.addTarget(self, action: #selector(footerButtonTapped), for: .touchUpInside)
result.isHidden = true
return result
}()
// MARK: - Initialization // MARK: - Initialization
init(viewModel: SessionTableViewModel<NavItemId, Section, SettingItem>) { init(viewModel: SessionTableViewModel<NavItemId, Section, SettingItem>) {
@ -74,6 +99,8 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
view.themeBackgroundColor = .backgroundPrimary view.themeBackgroundColor = .backgroundPrimary
view.addSubview(tableView) view.addSubview(tableView)
view.addSubview(fadeView)
view.addSubview(footerButton)
setupLayout() setupLayout()
setupBinding() setupBinding()
@ -114,6 +141,13 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
private func setupLayout() { private func setupLayout() {
tableView.pin(to: view) tableView.pin(to: view)
fadeView.pin(.leading, to: .leading, of: self.view)
fadeView.pin(.trailing, to: .trailing, of: self.view)
fadeView.pin(.bottom, to: .bottom, of: self.view)
footerButton.center(.horizontal, in: self.view)
footerButton.pin(.bottom, to: .bottom, of: self.view.safeAreaLayoutGuide, withInset: -Values.smallSpacing)
} }
// MARK: - Updating // MARK: - Updating
@ -257,6 +291,33 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
} }
.store(in: &disposables) .store(in: &disposables)
viewModel.footerButtonInfo
.receiveOnMain(immediately: true)
.sink { [weak self] buttonInfo in
if let buttonInfo: SessionButton.Info = buttonInfo {
self?.footerButton.setTitle(buttonInfo.title, for: .normal)
self?.footerButton.setStyle(buttonInfo.style)
self?.footerButton.isEnabled = buttonInfo.isEnabled
}
self?.onFooterTap = buttonInfo?.onTap
self?.fadeView.isHidden = (buttonInfo == nil)
self?.footerButton.isHidden = (buttonInfo == nil)
// If we have a footerButton then we want to manually control the contentInset
self?.tableView.contentInsetAdjustmentBehavior = (buttonInfo == nil ? .automatic : .never)
self?.tableView.contentInset = UIEdgeInsets(
top: 0,
left: 0,
bottom: (buttonInfo == nil ?
0 :
Values.footerGradientHeight(window: UIApplication.shared.keyWindow)
),
right: 0
)
}
.store(in: &disposables)
viewModel.showToast viewModel.showToast
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] text, color in .sink { [weak self] text, color in
@ -310,6 +371,10 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables) .store(in: &disposables)
} }
@objc private func footerButtonTapped() {
onFooterTap?()
}
// MARK: - UITableViewDataSource // MARK: - UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int { func numberOfSections(in tableView: UITableView) -> Int {

View File

@ -43,6 +43,9 @@ class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection,
preconditionFailure("abstract class - override in subclass") preconditionFailure("abstract class - override in subclass")
} }
open var footerView: AnyPublisher<UIView?, Never> { Just(nil).eraseToAnyPublisher() } open var footerView: AnyPublisher<UIView?, Never> { Just(nil).eraseToAnyPublisher() }
open var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> {
Just(nil).eraseToAnyPublisher()
}
func updateSettings(_ updatedSettings: [SectionModel]) { func updateSettings(_ updatedSettings: [SectionModel]) {
preconditionFailure("abstract class - override in subclass") preconditionFailure("abstract class - override in subclass")

View File

@ -5,6 +5,7 @@ import GRDB
import PromiseKit import PromiseKit
import WebRTC import WebRTC
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
public protocol WebRTCSessionDelegate: AnyObject { public protocol WebRTCSessionDelegate: AnyObject {
var videoCapturer: RTCVideoCapturer { get } var videoCapturer: RTCVideoCapturer { get }
@ -179,7 +180,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
uuid: uuid, uuid: uuid,
kind: .offer, kind: .offer,
sdps: [ sdp.sdp ], sdps: [ sdp.sdp ],
sentTimestampMs: UInt64(floor(Date().timeIntervalSince1970 * 1000)) sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs())
), ),
interactionId: nil, interactionId: nil,
in: thread in: thread

View File

@ -1286,7 +1286,7 @@ enum _003_YDBToGRDBMigration: Migration {
// so we can reverse-engineer an approximate timestamp by extracting it from // so we can reverse-engineer an approximate timestamp by extracting it from
// the id (this value is unlikely to match exactly though) // the id (this value is unlikely to match exactly though)
let fallbackTimestamp: UInt64 = legacyJob.id let fallbackTimestamp: UInt64 = legacyJob.id
.map { UInt64($0.prefix("\(Int(Date().timeIntervalSince1970 * 1000))".count)) } .map { UInt64($0.prefix("\(SnodeAPI.currentOffsetTimestampMs())".count)) }
.defaulting(to: 0) .defaulting(to: 0)
let legacyIdentifier: String = identifier( let legacyIdentifier: String = identifier(
for: threadId, for: threadId,
@ -1657,7 +1657,7 @@ enum _003_YDBToGRDBMigration: Migration {
state: .invalid, state: .invalid,
contentType: "", contentType: "",
byteCount: 0, byteCount: 0,
creationTimestamp: Date().timeIntervalSince1970, creationTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000),
sourceFilename: nil, sourceFilename: nil,
downloadUrl: nil, downloadUrl: nil,
localRelativeFilePath: nil, localRelativeFilePath: nil,

View File

@ -1,12 +1,13 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import AVFAudio
import AVFoundation
import GRDB import GRDB
import PromiseKit import PromiseKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit import SessionUtilitiesKit
import AVFAudio import SessionSnodeKit
import AVFoundation
public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "attachment" } public static var databaseTableName: String { "attachment" }
@ -1062,7 +1063,7 @@ extension Attachment {
// Check the file size // Check the file size
SNLog("File size: \(data.count) bytes.") SNLog("File size: \(data.count) bytes.")
if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier { if data.count > FileServerAPI.maxFileSize {
failure?(HTTP.Error.maxFileSizeExceeded) failure?(HTTP.Error.maxFileSizeExceeded)
return return
} }
@ -1114,7 +1115,7 @@ extension Attachment {
state: .uploaded, state: .uploaded,
creationTimestamp: ( creationTimestamp: (
updatedAttachment?.creationTimestamp ?? updatedAttachment?.creationTimestamp ??
Date().timeIntervalSince1970 (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
), ),
downloadUrl: "\(FileServerAPI.server)/files/\(fileId)" downloadUrl: "\(FileServerAPI.server)/files/\(fileId)"
) )

View File

@ -3,6 +3,7 @@
import Foundation import Foundation
import GRDB import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
/// We can rely on the unique constraints within the `Interaction` table to prevent duplicate `VisibleMessage` /// We can rely on the unique constraints within the `Interaction` table to prevent duplicate `VisibleMessage`
/// values from being processed, but some control messages dont have an associated interaction - this table provides /// values from being processed, but some control messages dont have an associated interaction - this table provides
@ -168,7 +169,10 @@ internal extension ControlMessageProcessRecord {
self.threadId = threadId self.threadId = threadId
self.timestampMs = timestampMs 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 /// 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`) /// clean out these excessive entries after `defaultExpirationSeconds`)
static func generateLegacyProcessRecords(_ db: Database, receivedMessageTimestamps: [Int64]) throws { static func generateLegacyProcessRecords(_ db: Database, receivedMessageTimestamps: [Int64]) throws {
let defaultExpirationTimestamp: TimeInterval = ( let defaultExpirationTimestamp: TimeInterval = (
Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
ControlMessageProcessRecord.defaultExpirationSeconds
) )
try receivedMessageTimestamps.forEach { timestampMs in try receivedMessageTimestamps.forEach { timestampMs in

View File

@ -3,6 +3,7 @@
import Foundation import Foundation
import GRDB import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "disappearingMessagesConfiguration" } public static var databaseTableName: String { "disappearingMessagesConfiguration" }
@ -206,7 +207,7 @@ public class SMKDisappearingMessagesConfiguration: NSObject {
authorId: getUserHexEncodedPublicKey(db), authorId: getUserHexEncodedPublicKey(db),
variant: .infoDisappearingMessagesUpdate, variant: .infoDisappearingMessagesUpdate,
body: config.messageInfoString(with: nil), body: config.messageInfoString(with: nil),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
) )
.inserted(db) .inserted(db)

View File

@ -4,6 +4,7 @@ import Foundation
import GRDB import GRDB
import Sodium import Sodium
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible { public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "interaction" } public static var databaseTableName: String { "interaction" }
@ -298,7 +299,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
self.timestampMs = timestampMs self.timestampMs = timestampMs
self.receivedAtTimestampMs = { self.receivedAtTimestampMs = {
switch variant { 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 /// For TSInteractions which are not `standardIncoming` and `standardOutgoing` use the `timestampMs` value
default: return timestampMs default: return timestampMs
@ -458,7 +459,7 @@ public extension Interaction {
job: DisappearingMessagesJob.updateNextRunIfNeeded( job: DisappearingMessagesJob.updateNextRunIfNeeded(
db, db,
interactionIds: interactionIds, interactionIds: interactionIds,
startedAtMs: (Date().timeIntervalSince1970 * 1000) startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
) )
) )

View File

@ -6,6 +6,7 @@ import PromiseKit
import AFNetworking import AFNetworking
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "linkPreview" } public static var databaseTableName: String { "linkPreview" }
@ -60,7 +61,7 @@ public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, Persis
public init( public init(
url: String, url: String,
timestamp: TimeInterval = LinkPreview.timestampFor( timestamp: TimeInterval = LinkPreview.timestampFor(
sentTimestampMs: (Date().timeIntervalSince1970 * 1000) // Default to now sentTimestampMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs()) // Default to now
), ),
variant: Variant = .standard, variant: Variant = .standard,
title: String?, title: String?,

View File

@ -65,16 +65,37 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe
} }
} }
public func statusIconInfo(variant: Interaction.Variant, hasAtLeastOneReadReceipt: Bool) -> (image: UIImage?, themeTintColor: ThemeValue) { public func statusIconInfo(variant: Interaction.Variant, hasAtLeastOneReadReceipt: Bool) -> (image: UIImage?, text: String?, themeTintColor: ThemeValue) {
guard variant == .standardOutgoing else { return (nil, .textPrimary) } guard variant == .standardOutgoing else { return (nil, nil, .textPrimary) }
switch (self, hasAtLeastOneReadReceipt) { 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, _): 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, _):
case (.failed, _): return (UIImage(systemName: "exclamationmark.circle"), .danger) return (
UIImage(systemName: "exclamationmark.triangle"),
"MESSAGE_DELIVERY_STATUS_FAILED".localized(),
.danger
)
} }
} }
} }

View File

@ -4,6 +4,7 @@ import Foundation
import GRDB import GRDB
import Sodium import Sodium
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "thread" } public static var databaseTableName: String { "thread" }
@ -104,7 +105,7 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
public init( public init(
id: String, id: String,
variant: Variant, variant: Variant,
creationDateTimestamp: TimeInterval = Date().timeIntervalSince1970, creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000),
shouldBeVisible: Bool = false, shouldBeVisible: Bool = false,
isPinned: Bool = false, isPinned: Bool = false,
messageDraft: String? = nil, messageDraft: String? = nil,

View File

@ -15,13 +15,9 @@ public final class FileServerAPI: NSObject {
@objc public static let server = "http://filev2.getsession.org" @objc public static let server = "http://filev2.getsession.org"
public static let serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" public static let serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
public static let maxFileSize = (10 * 1024 * 1024) // 10 MB 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 /// Standard timeout is 10 seconds which is a little too short fir file upload/download with slightly larger files
/// 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 public static let fileTimeout: TimeInterval = 30
/// 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
// MARK: - File Storage // MARK: - File Storage
@ -77,7 +73,7 @@ public final class FileServerAPI: NSObject {
return Promise(error: error) 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 .map2 { _, response in
guard let response: Data = response else { throw HTTP.Error.parsingFailed } guard let response: Data = response else { throw HTTP.Error.parsingFailed }

View File

@ -145,7 +145,7 @@ public enum AttachmentDownloadJob: JobExecutor {
_ = try attachment _ = try attachment
.with( .with(
state: .downloaded, state: .downloaded,
creationTimestamp: Date().timeIntervalSince1970, creationTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000),
localRelativeFilePath: ( localRelativeFilePath: (
attachment.localRelativeFilePath ?? attachment.localRelativeFilePath ??
Attachment.localRelativeFilePath(from: attachment.originalFilePath) Attachment.localRelativeFilePath(from: attachment.originalFilePath)

View File

@ -3,6 +3,7 @@
import Foundation import Foundation
import GRDB import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
public enum DisappearingMessagesJob: JobExecutor { public enum DisappearingMessagesJob: JobExecutor {
public static let maxFailureCount: Int = -1 public static let maxFailureCount: Int = -1
@ -17,7 +18,7 @@ public enum DisappearingMessagesJob: JobExecutor {
deferred: @escaping (Job) -> () deferred: @escaping (Job) -> ()
) { ) {
// The 'backgroundTask' gets captured and cleared within the 'completion' block // 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) var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: #function)
let updatedJob: Job? = Storage.shared.write { db in let updatedJob: Job? = Storage.shared.write { db in
@ -59,10 +60,14 @@ public extension DisappearingMessagesJob {
guard let nextExpirationTimestampMs: Double = nextExpirationTimestampMs else { return nil } 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 return try? Job
.filter(Job.Columns.variant == Job.Variant.disappearingMessages) .filter(Job.Columns.variant == Job.Variant.disappearingMessages)
.fetchOne(db)? .fetchOne(db)?
.with(nextRunTimestamp: ceil(nextExpirationTimestampMs / 1000)) .with(nextRunTimestamp: ceil((nextExpirationTimestampMs - Double(clockOffsetMs)) / 1000))
.saved(db) .saved(db)
} }

View File

@ -96,7 +96,7 @@ public enum GarbageCollectionJob: JobExecutor {
GROUP BY \(interaction[.threadId]) GROUP BY \(interaction[.threadId])
) AS interactionInfo ON interactionInfo.\(threadIdLiteral) = \(interaction[.threadId]) ) AS interactionInfo ON interactionInfo.\(threadIdLiteral) = \(interaction[.threadId])
WHERE ( WHERE (
\(interaction[.timestampMs]) < \(timestampNow - approxSixMonthsInSeconds) AND \(interaction[.timestampMs]) < \((timestampNow - approxSixMonthsInSeconds) * 1000) AND
interactionInfo.interactionCount >= \(minInteractionsToTrimSql) interactionInfo.interactionCount >= \(minInteractionsToTrimSql)
) )
) )

View File

@ -259,7 +259,10 @@ public extension Message {
return try processRawReceivedMessage( return try processRawReceivedMessage(
db, db,
envelope: envelope, envelope: envelope,
serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds), serverExpirationTimestamp: (
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
ControlMessageProcessRecord.defaultExpirationSeconds
),
serverHash: serverHash, serverHash: serverHash,
handleClosedGroupKeyUpdateMessages: true handleClosedGroupKeyUpdateMessages: true
) )
@ -275,7 +278,10 @@ public extension Message {
let processedMessage: ProcessedMessage? = try processRawReceivedMessage( let processedMessage: ProcessedMessage? = try processRawReceivedMessage(
db, db,
envelope: envelope, envelope: envelope,
serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds), serverExpirationTimestamp: (
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
ControlMessageProcessRecord.defaultExpirationSeconds
),
serverHash: nil, serverHash: nil,
handleClosedGroupKeyUpdateMessages: false handleClosedGroupKeyUpdateMessages: false
) )
@ -407,7 +413,7 @@ public extension Message {
let count: Int64 = rawReaction.you ? rawReaction.count - 1 : rawReaction.count 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 maxLength: Int = shouldAddSelfReaction ? 4 : 5
let desiredReactorIds: [String] = reactors let desiredReactorIds: [String] = reactors
.filter { $0 != blindedUserPublicKey && $0 != userPublicKey } // Remove current user for now, will add back if needed .filter { $0 != blindedUserPublicKey && $0 != userPublicKey } // Remove current user for now, will add back if needed

View File

@ -871,6 +871,7 @@ public enum OpenGroupAPI {
], ],
body: bytes body: bytes
), ),
timeout: FileServerAPI.fileTimeout,
using: dependencies using: dependencies
) )
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) .decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
@ -890,6 +891,7 @@ public enum OpenGroupAPI {
server: server, server: server,
endpoint: .roomFileIndividual(roomToken, fileId) endpoint: .roomFileIndividual(roomToken, fileId)
), ),
timeout: FileServerAPI.fileTimeout,
using: dependencies using: dependencies
) )
.map { responseInfo, maybeData in .map { responseInfo, maybeData in
@ -1391,6 +1393,7 @@ public enum OpenGroupAPI {
_ db: Database, _ db: Database,
request: Request<T, Endpoint>, request: Request<T, Endpoint>,
forceBlinded: Bool = false, forceBlinded: Bool = false,
timeout: TimeInterval = HTTP.timeout,
using dependencies: SMKDependencies = SMKDependencies() using dependencies: SMKDependencies = SMKDependencies()
) -> Promise<(OnionRequestResponseInfoType, Data?)> { ) -> Promise<(OnionRequestResponseInfoType, Data?)> {
let urlRequest: URLRequest let urlRequest: URLRequest
@ -1415,6 +1418,6 @@ public enum OpenGroupAPI {
return Promise(error: OpenGroupAPIError.signingFailed) 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)
} }
} }

View File

@ -4,6 +4,7 @@ import Foundation
import GRDB import GRDB
import WebRTC import WebRTC
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
extension MessageReceiver { extension MessageReceiver {
public static func handleCallMessage(_ db: Database, message: CallMessage) throws { public static func handleCallMessage(_ db: Database, message: CallMessage) throws {
@ -189,7 +190,7 @@ extension MessageReceiver {
body: String(data: messageInfoData, encoding: .utf8), body: String(data: messageInfoData, encoding: .utf8),
timestampMs: ( timestampMs: (
message.sentTimestamp.map { Int64($0) } ?? message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000)) SnodeAPI.currentOffsetTimestampMs()
) )
) )
.inserted(db) .inserted(db)
@ -235,7 +236,7 @@ extension MessageReceiver {
) )
let timestampMs: Int64 = ( let timestampMs: Int64 = (
message.sentTimestamp.map { Int64($0) } ?? message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000)) SnodeAPI.currentOffsetTimestampMs()
) )
guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil } guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil }

View File

@ -4,6 +4,7 @@ import Foundation
import GRDB import GRDB
import Sodium import Sodium
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
extension MessageReceiver { extension MessageReceiver {
public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage) throws { public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage) throws {
@ -135,7 +136,7 @@ extension MessageReceiver {
threadId: groupPublicKey, threadId: groupPublicKey,
publicKey: Data(encryptionKeyPair.publicKey), publicKey: Data(encryptionKeyPair.publicKey),
secretKey: Data(encryptionKeyPair.secretKey), secretKey: Data(encryptionKeyPair.secretKey),
receivedTimestamp: Date().timeIntervalSince1970 receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
).insert(db) ).insert(db)
// Start polling // Start polling
@ -196,7 +197,7 @@ extension MessageReceiver {
threadId: groupPublicKey, threadId: groupPublicKey,
publicKey: proto.publicKey.removingIdPrefixIfNeeded(), publicKey: proto.publicKey.removingIdPrefixIfNeeded(),
secretKey: proto.privateKey, secretKey: proto.privateKey,
receivedTimestamp: Date().timeIntervalSince1970 receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
).insert(db) ).insert(db)
} }
catch { catch {
@ -231,7 +232,7 @@ extension MessageReceiver {
.infoMessage(db, sender: sender), .infoMessage(db, sender: sender),
timestampMs: ( timestampMs: (
message.sentTimestamp.map { Int64($0) } ?? message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000)) SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
} }
@ -307,7 +308,7 @@ extension MessageReceiver {
.infoMessage(db, sender: sender), .infoMessage(db, sender: sender),
timestampMs: ( timestampMs: (
message.sentTimestamp.map { Int64($0) } ?? message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000)) SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
} }
@ -383,7 +384,7 @@ extension MessageReceiver {
.infoMessage(db, sender: sender), .infoMessage(db, sender: sender),
timestampMs: ( timestampMs: (
message.sentTimestamp.map { Int64($0) } ?? message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000)) SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
} }
@ -461,7 +462,7 @@ extension MessageReceiver {
.infoMessage(db, sender: sender), .infoMessage(db, sender: sender),
timestampMs: ( timestampMs: (
message.sentTimestamp.map { Int64($0) } ?? message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000)) SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
} }

View File

@ -2,6 +2,7 @@
import Foundation import Foundation
import GRDB import GRDB
import SessionSnodeKit
extension MessageReceiver { extension MessageReceiver {
internal static func handleDataExtractionNotification(_ db: Database, message: DataExtractionNotification) throws { internal static func handleDataExtractionNotification(_ db: Database, message: DataExtractionNotification) throws {
@ -24,7 +25,7 @@ extension MessageReceiver {
}(), }(),
timestampMs: ( timestampMs: (
message.sentTimestamp.map { Int64($0) } ?? message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000)) SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
} }

View File

@ -4,6 +4,7 @@ import Foundation
import GRDB import GRDB
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
extension MessageReceiver { extension MessageReceiver {
internal static func handleMessageRequestResponse( internal static func handleMessageRequestResponse(
@ -123,7 +124,7 @@ extension MessageReceiver {
variant: .infoMessageRequestAccepted, variant: .infoMessageRequestAccepted,
timestampMs: ( timestampMs: (
message.sentTimestamp.map { Int64($0) } ?? message.sentTimestamp.map { Int64($0) } ??
Int64(floor(Date().timeIntervalSince1970 * 1000)) SnodeAPI.currentOffsetTimestampMs()
) )
).inserted(db) ).inserted(db)
} }

View File

@ -6,6 +6,7 @@ import Sodium
import Curve25519Kit import Curve25519Kit
import PromiseKit import PromiseKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
extension MessageSender { extension MessageSender {
public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:]) public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:])
@ -24,7 +25,7 @@ extension MessageSender {
let membersAsData = members.map { Data(hex: $0) } let membersAsData = members.map { Data(hex: $0) }
let admins = [ userPublicKey ] let admins = [ userPublicKey ]
let adminsAsData = admins.map { Data(hex: $0) } let adminsAsData = admins.map { Data(hex: $0) }
let formationTimestamp: TimeInterval = Date().timeIntervalSince1970 let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: groupPublicKey, variant: .closedGroup) .fetchOrCreate(db, id: groupPublicKey, variant: .closedGroup)
try ClosedGroup( try ClosedGroup(
@ -91,7 +92,7 @@ extension MessageSender {
threadId: groupPublicKey, threadId: groupPublicKey,
publicKey: encryptionKeyPair.publicKey, publicKey: encryptionKeyPair.publicKey,
secretKey: encryptionKeyPair.privateKey, secretKey: encryptionKeyPair.privateKey,
receivedTimestamp: Date().timeIntervalSince1970 receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
).insert(db) ).insert(db)
// Notify the PN server // Notify the PN server
@ -110,7 +111,7 @@ extension MessageSender {
threadId: thread.id, threadId: thread.id,
authorId: userPublicKey, authorId: userPublicKey,
variant: .infoClosedGroupCreated, variant: .infoClosedGroupCreated,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db) ).inserted(db)
// Start polling // Start polling
@ -142,7 +143,7 @@ extension MessageSender {
threadId: closedGroup.threadId, threadId: closedGroup.threadId,
publicKey: legacyNewKeyPair.publicKey, publicKey: legacyNewKeyPair.publicKey,
secretKey: legacyNewKeyPair.privateKey, secretKey: legacyNewKeyPair.privateKey,
receivedTimestamp: Date().timeIntervalSince1970 receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
) )
// Distribute it // Distribute it
@ -230,7 +231,7 @@ extension MessageSender {
body: ClosedGroupControlMessage.Kind body: ClosedGroupControlMessage.Kind
.nameChange(name: name) .nameChange(name: name)
.infoMessage(db, sender: userPublicKey), .infoMessage(db, sender: userPublicKey),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db) ).inserted(db)
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
@ -330,7 +331,7 @@ extension MessageSender {
body: ClosedGroupControlMessage.Kind body: ClosedGroupControlMessage.Kind
.membersAdded(members: addedMembers.map { Data(hex: $0) }) .membersAdded(members: addedMembers.map { Data(hex: $0) })
.infoMessage(db, sender: userPublicKey), .infoMessage(db, sender: userPublicKey),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db) ).inserted(db)
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
@ -431,7 +432,7 @@ extension MessageSender {
body: ClosedGroupControlMessage.Kind body: ClosedGroupControlMessage.Kind
.membersRemoved(members: removedMembers.map { Data(hex: $0) }) .membersRemoved(members: removedMembers.map { Data(hex: $0) })
.infoMessage(db, sender: userPublicKey), .infoMessage(db, sender: userPublicKey),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db) ).inserted(db)
guard let newInteractionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } guard let newInteractionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
@ -496,7 +497,7 @@ extension MessageSender {
body: ClosedGroupControlMessage.Kind body: ClosedGroupControlMessage.Kind
.memberLeft .memberLeft
.infoMessage(db, sender: userPublicKey), .infoMessage(db, sender: userPublicKey),
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db) ).inserted(db)
guard let interactionId: Int64 = interaction.id else { guard let interactionId: Int64 = interaction.id else {

View File

@ -5,6 +5,7 @@ import GRDB
import Sodium import Sodium
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
public enum MessageReceiver { public enum MessageReceiver {
private static var lastEncryptionKeyPairRequest: [String: Date] = [:] private static var lastEncryptionKeyPairRequest: [String: Date] = [:]
@ -144,7 +145,7 @@ public enum MessageReceiver {
message.sender = sender message.sender = sender
message.recipient = userPublicKey message.recipient = userPublicKey
message.sentTimestamp = envelope.timestamp message.sentTimestamp = envelope.timestamp
message.receivedTimestamp = UInt64((Date().timeIntervalSince1970) * 1000) message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
message.groupPublicKey = groupPublicKey message.groupPublicKey = groupPublicKey
message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) } message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) }
@ -318,7 +319,7 @@ public enum MessageReceiver {
var updatedProfile: Profile = profile var updatedProfile: Profile = profile
// Name // Name
if let name = name, name != profile.name { if let name = name, !name.isEmpty, name != profile.name {
let shouldUpdate: Bool let shouldUpdate: Bool
if isCurrentUser { if isCurrentUser {
shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) { shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) {

View File

@ -66,8 +66,7 @@ public final class MessageSender {
) throws -> Promise<Void> { ) throws -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending() let (promise, seal) = Promise<Void>.pending()
let userPublicKey: String = getUserHexEncodedPublicKey(db) let userPublicKey: String = getUserHexEncodedPublicKey(db)
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) let messageSendTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
let messageSendTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
// Set the timestamp, sender and recipient // Set the timestamp, sender and recipient
message.sentTimestamp = ( message.sentTimestamp = (
@ -202,7 +201,7 @@ public final class MessageSender {
recipient: message.recipient!, recipient: message.recipient!,
data: base64EncodedData, data: base64EncodedData,
ttl: message.ttl, ttl: message.ttl,
timestampMs: UInt64(messageSendTimestamp + SnodeAPI.clockOffset.wrappedValue) timestampMs: UInt64(messageSendTimestamp)
) )
SnodeAPI SnodeAPI
@ -261,6 +260,8 @@ public final class MessageSender {
behaviour: .runOnce, behaviour: .runOnce,
details: NotifyPushServerJob.Details(message: snodeMessage) details: NotifyPushServerJob.Details(message: snodeMessage)
) )
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive])
.defaulting(to: false)
if isMainAppActive { if isMainAppActive {
JobRunner.add(db, job: job) JobRunner.add(db, job: job)
@ -322,7 +323,7 @@ public final class MessageSender {
// Set the timestamp, sender and recipient // Set the timestamp, sender and recipient
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set 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 { switch destination {
@ -472,7 +473,7 @@ public final class MessageSender {
// Set the timestamp, sender and recipient // Set the timestamp, sender and recipient
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set 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 message.sender = userPublicKey
@ -617,7 +618,7 @@ public final class MessageSender {
job: DisappearingMessagesJob.updateNextRunIfNeeded( job: DisappearingMessagesJob.updateNextRunIfNeeded(
db, db,
interaction: interaction, interaction: interaction,
startedAtMs: (Date().timeIntervalSince1970 * 1000) startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
) )
) )
} }
@ -636,7 +637,10 @@ public final class MessageSender {
} }
}(), }(),
message: message, message: message,
serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds) serverExpirationTimestamp: (
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
ControlMessageProcessRecord.defaultExpirationSeconds
)
)?.insert(db) )?.insert(db)
// Sync the message if: // Sync the message if:

View File

@ -3,6 +3,7 @@
import Foundation import Foundation
import GRDB import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
public class TypingIndicators { public class TypingIndicators {
// MARK: - Direction // MARK: - Direction
@ -41,7 +42,7 @@ public class TypingIndicators {
self.threadId = threadId self.threadId = threadId
self.direction = direction self.direction = direction
self.timestampMs = (timestampMs ?? Int64(floor(Date().timeIntervalSince1970 * 1000))) self.timestampMs = (timestampMs ?? SnodeAPI.currentOffsetTimestampMs())
} }
fileprivate func start(_ db: Database) { fileprivate func start(_ db: Database) {

View File

@ -196,7 +196,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
authorId: getUserHexEncodedPublicKey(db), authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing, variant: .standardOutgoing,
body: body, body: body,
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), timestampMs: SnodeAPI.currentOffsetTimestampMs(),
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body), hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body),
expiresInSeconds: try? DisappearingMessagesConfiguration expiresInSeconds: try? DisappearingMessagesConfiguration
.select(.durationSeconds) .select(.durationSeconds)

View File

@ -93,7 +93,7 @@ public extension SnodeReceivedMessageInfo {
return try SnodeReceivedMessageInfo return try SnodeReceivedMessageInfo
.select(Column.rowID) .select(Column.rowID)
.filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey, namespace: namespace)) .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) .asRequest(of: Int64.self)
.fetchAll(db) .fetchAll(db)
} }
@ -122,7 +122,7 @@ public extension SnodeReceivedMessageInfo {
SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid == false SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid == false
) )
.filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey, namespace: namespace)) .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) .order(SnodeReceivedMessageInfo.Columns.id.desc)
.fetchOne(db) .fetchOne(db)

View File

@ -7,13 +7,17 @@ import PromiseKit
import SessionUtilitiesKit import SessionUtilitiesKit
public protocol OnionRequestAPIType { public protocol OnionRequestAPIType {
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data> static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?, timeout: TimeInterval) -> Promise<Data>
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String, timeout: TimeInterval) -> Promise<(OnionRequestResponseInfoType, Data?)>
} }
public extension OnionRequestAPIType { public extension OnionRequestAPIType {
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data> {
sendOnionRequest(request, to: server, using: .v4, with: x25519PublicKey) 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 // MARK: - Public API
/// Sends an onion request to `snode`. Builds new paths as needed. /// 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<Data> { public static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise<Data> {
let payloadJson: JSON = [ "method" : method.rawValue, "params" : parameters ] let payloadJson: JSON = [ "method" : method.rawValue, "params" : parameters ]
guard let payload: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []) else { 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 /// **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 .map { _, maybeData in
guard let data: Data = maybeData else { throw HTTP.Error.invalidResponse } 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. /// 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 { guard let url = request.url, let host = request.url?.host else {
return Promise(error: OnionRequestAPIError.invalidURL) return Promise(error: OnionRequestAPIError.invalidURL)
} }
@ -412,14 +416,14 @@ public enum OnionRequestAPI: OnionRequestAPIType {
scheme: scheme, scheme: scheme,
port: port 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 promise.catch2 { error in
SNLog("Couldn't reach server: \(url) due to error: \(error).") SNLog("Couldn't reach server: \(url) due to error: \(error).")
} }
return promise 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() let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending()
var guardSnode: Snode? var guardSnode: Snode?
@ -444,7 +448,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
} }
let destinationSymmetricKey = intermediate.destinationSymmetricKey let destinationSymmetricKey = intermediate.destinationSymmetricKey
HTTP.execute(.post, url, body: body) HTTP.execute(.post, url, body: body, timeout: timeout)
.done2 { responseData in .done2 { responseData in
handleResponse( handleResponse(
responseData: responseData, responseData: responseData,
@ -672,7 +676,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
if let timestamp = body["t"] as? Int64 { if let timestamp = body["t"] as? Int64 {
let offset = timestamp - Int64(floor(Date().timeIntervalSince1970 * 1000)) let offset = timestamp - Int64(floor(Date().timeIntervalSince1970 * 1000))
SnodeAPI.clockOffset.mutate { $0 = offset } SnodeAPI.clockOffsetMs.mutate { $0 = offset }
} }
guard 200...299 ~= statusCode else { guard 200...299 ~= statusCode else {

View File

@ -19,10 +19,16 @@ public final class SnodeAPI {
internal static var snodePool: Atomic<Set<Snode>> = Atomic([]) internal static var snodePool: Atomic<Set<Snode>> = Atomic([])
/// The offset between the user's clock and the Service Node's clock. Used in cases where the /// The offset between the user's clock and the Service Node's clock. Used in cases where the
/// user's clock is incorrect. /// user's clock is incorrect
/// public static var clockOffsetMs: Atomic<Int64> = Atomic(0)
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
public static var clockOffset: Atomic<Int64> = 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. /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
public static var swarmCache: Atomic<[String: Set<Snode>]> = Atomic([:]) public static var swarmCache: Atomic<[String: Set<Snode>]> = Atomic([:])
@ -546,7 +552,7 @@ public final class SnodeAPI {
let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, namespace: namespace, associatedWith: publicKey)?.hash ?? "" let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, namespace: namespace, associatedWith: publicKey)?.hash ?? ""
// Construct signature // Construct signature
let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue) let timestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString() let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
let namespaceVerificationString = (namespace == defaultNamespace ? "" : String(namespace)) let namespaceVerificationString = (namespace == defaultNamespace ? "" : String(namespace))
@ -647,7 +653,7 @@ public final class SnodeAPI {
} }
// Construct signature // Construct signature
let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue) let timestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString() let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
guard guard
@ -1102,3 +1108,11 @@ public final class SnodeAPI {
return nil return nil
} }
} }
@objc(SNSnodeAPI)
public final class SNSnodeAPI: NSObject {
@objc(currentOffsetTimestampMs)
public static func currentOffsetTimestampMs() -> UInt64 {
return UInt64(SnodeAPI.currentOffsetTimestampMs())
}
}

View File

@ -17,6 +17,25 @@ public final class SessionButton: UIButton {
case large 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 private let style: Style
public override var isEnabled: Bool { public override var isEnabled: Bool {
@ -157,4 +176,10 @@ public final class SessionButton: UIButton {
} }
}() }()
} }
// MARK: - Functions
public func setStyle(_ style: Style) {
setup(style: style)
}
} }

View File

@ -34,6 +34,7 @@ internal enum Theme_ClassicDark: ThemeColors {
.messageBubble_outgoingText: .classicDark0, .messageBubble_outgoingText: .classicDark0,
.messageBubble_incomingText: .classicDark6, .messageBubble_incomingText: .classicDark6,
.messageBubble_overlay: .black_06, .messageBubble_overlay: .black_06,
.messageBubble_deliveryStatus: .classicDark5,
// MenuButton // MenuButton
.menuButton_background: .primary, .menuButton_background: .primary,

View File

@ -34,6 +34,7 @@ internal enum Theme_ClassicLight: ThemeColors {
.messageBubble_outgoingText: .classicLight0, .messageBubble_outgoingText: .classicLight0,
.messageBubble_incomingText: .classicLight0, .messageBubble_incomingText: .classicLight0,
.messageBubble_overlay: .black_06, .messageBubble_overlay: .black_06,
.messageBubble_deliveryStatus: .classicLight1,
// MenuButton // MenuButton
.menuButton_background: .primary, .menuButton_background: .primary,

View File

@ -34,6 +34,7 @@ internal enum Theme_OceanDark: ThemeColors {
.messageBubble_outgoingText: .oceanDark0, .messageBubble_outgoingText: .oceanDark0,
.messageBubble_incomingText: .oceanDark7, .messageBubble_incomingText: .oceanDark7,
.messageBubble_overlay: .black_06, .messageBubble_overlay: .black_06,
.messageBubble_deliveryStatus: .oceanDark5,
// MenuButton // MenuButton
.menuButton_background: .primary, .menuButton_background: .primary,

View File

@ -34,6 +34,7 @@ internal enum Theme_OceanLight: ThemeColors {
.messageBubble_outgoingText: .oceanLight1, .messageBubble_outgoingText: .oceanLight1,
.messageBubble_incomingText: .oceanLight1, .messageBubble_incomingText: .oceanLight1,
.messageBubble_overlay: .black_06, .messageBubble_overlay: .black_06,
.messageBubble_deliveryStatus: .oceanLight2,
// MenuButton // MenuButton
.menuButton_background: .primary, .menuButton_background: .primary,

View File

@ -122,6 +122,7 @@ public indirect enum ThemeValue: Hashable {
case messageBubble_outgoingText case messageBubble_outgoingText
case messageBubble_incomingText case messageBubble_incomingText
case messageBubble_overlay case messageBubble_overlay
case messageBubble_deliveryStatus
// MenuButton // MenuButton
case menuButton_background case menuButton_background

View File

@ -53,7 +53,7 @@ extension ECKeyPair {
public extension Identity { public extension Identity {
static func generate(from seed: Data) throws -> (ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) { 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) let padding = Data(repeating: 0, count: 16)
guard guard

View File

@ -19,6 +19,7 @@ public enum General {
} }
public enum GeneralError: Error { public enum GeneralError: Error {
case invalidSeed
case keyGenerationFailed case keyGenerationFailed
} }

View File

@ -111,6 +111,7 @@ public final class JobRunner {
fileprivate static var perSessionJobsCompleted: Atomic<Set<Int64>> = Atomic([]) fileprivate static var perSessionJobsCompleted: Atomic<Set<Int64>> = Atomic([])
private static var hasCompletedInitialBecomeActive: Atomic<Bool> = Atomic(false) private static var hasCompletedInitialBecomeActive: Atomic<Bool> = Atomic(false)
private static var shutdownBackgroundTask: Atomic<OWSBackgroundTask?> = Atomic(nil) private static var shutdownBackgroundTask: Atomic<OWSBackgroundTask?> = Atomic(nil)
fileprivate static var canStartQueues: Atomic<Bool> = Atomic(false)
// MARK: - Configuration // MARK: - Configuration
@ -161,6 +162,9 @@ public final class JobRunner {
queues.wrappedValue[job.variant]?.upsert(job, canStartJob: canStartJob) 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 // Start the job runner if needed
db.afterNextTransaction { _ in db.afterNextTransaction { _ in
queues.wrappedValue[job.variant]?.start() queues.wrappedValue[job.variant]?.start()
@ -188,15 +192,13 @@ public final class JobRunner {
queues.wrappedValue[updatedJob.variant]?.insert(updatedJob, before: otherJob) 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) return (jobId, updatedJob)
} }
public static func appDidFinishLaunching() { 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 // Note: 'appDidBecomeActive' will run on first launch anyway so we can
// leave those jobs out and can wait until then to start the JobRunner // leave those jobs out and can wait until then to start the JobRunner
let jobsToRun: (blocking: [Job], nonBlocking: [Job]) = Storage.shared let jobsToRun: (blocking: [Job], nonBlocking: [Job]) = Storage.shared
@ -242,6 +244,9 @@ public final class JobRunner {
} }
public static func appDidBecomeActive() { 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 // 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 // can result in the database being suspended and us being unable to interact with it at all
shutdownBackgroundTask.mutate { shutdownBackgroundTask.mutate {
@ -291,6 +296,11 @@ public final class JobRunner {
exceptForVariant: Job.Variant? = nil, exceptForVariant: Job.Variant? = nil,
onComplete: (() -> ())? = 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` // Stop all queues except for the one containing the `exceptForVariant`
queues.wrappedValue queues.wrappedValue
.values .values
@ -632,6 +642,7 @@ private final class JobQueue {
fileprivate func start(force: Bool = false) { fileprivate func start(force: Bool = false) {
// We only want the JobRunner to run in the main app // We only want the JobRunner to run in the main app
guard CurrentAppContext().isMainApp else { return } guard CurrentAppContext().isMainApp else { return }
guard JobRunner.canStartQueues.wrappedValue else { return }
guard force || !isRunning.wrappedValue else { return } guard force || !isRunning.wrappedValue else { return }
// The JobRunner runs synchronously we need to ensure this doesn't start // The JobRunner runs synchronously we need to ensure this doesn't start
@ -852,8 +863,9 @@ private final class JobQueue {
.fetchOne(db) .fetchOne(db)
} }
// If there are no remaining jobs the trigger the 'onQueueDrained' callback and stop // If there are no remaining jobs or the JobRunner isn't allowed to start any queues then trigger
guard let nextJobTimestamp: TimeInterval = nextJobTimestamp else { // the 'onQueueDrained' callback and stop
guard let nextJobTimestamp: TimeInterval = nextJobTimestamp, JobRunner.canStartQueues.wrappedValue else {
if executionType != .concurrent || jobsCurrentlyRunning.wrappedValue.isEmpty { if executionType != .concurrent || jobsCurrentlyRunning.wrappedValue.isEmpty {
self.onQueueDrained?() self.onQueueDrained?()
} }
@ -1064,6 +1076,8 @@ private final class JobQueue {
queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) } queue = queue.filter { !dependantJobIds.contains($0.id ?? -1) }
} }
} }
performCleanUp(for: job, result: .failed)
return return
} }
@ -1082,7 +1096,7 @@ private final class JobQueue {
try job.dependantJobs try job.dependantJobs
.updateAll( .updateAll(
db, db,
Job.Columns.failureCount.set(to: job.failureCount), Job.Columns.failureCount.set(to: (job.failureCount + 1)),
Job.Columns.nextRunTimestamp.set(to: (nextRunTimestamp + (1 / 1000))) Job.Columns.nextRunTimestamp.set(to: (nextRunTimestamp + (1 / 1000)))
) )

View File

@ -8,10 +8,7 @@ import SessionMessagingKit
public enum Configuration { public enum Configuration {
public static func performMainSetup() { public static func performMainSetup() {
// Need to do this first to ensure the legacy database exists // Need to do this first to ensure the legacy database exists
SNUtilitiesKit.configure( SNUtilitiesKit.configure(maxFileSize: UInt(FileServerAPI.maxFileSize))
maxFileSize: UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier)
)
SNMessagingKit.configure() SNMessagingKit.configure()
SNSnodeKit.configure() SNSnodeKit.configure()
SNUIKit.configure() SNUIKit.configure()