Merge branch 'dev' into accessibility_ids_3
This commit is contained in:
commit
ec0b35a9a2
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 ||
|
||||||
(
|
(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 don’t have an associated interaction - this table provides
|
/// values from being processed, but some control messages don’t have an associated interaction - this table provides
|
||||||
|
@ -168,7 +169,10 @@ internal extension ControlMessageProcessRecord {
|
||||||
|
|
||||||
self.threadId = threadId
|
self.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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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?,
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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]) {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -19,6 +19,7 @@ public enum General {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GeneralError: Error {
|
public enum GeneralError: Error {
|
||||||
|
case invalidSeed
|
||||||
case keyGenerationFailed
|
case keyGenerationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue