Merge remote-tracking branch 'upstream/dev' into fix/new-certificates

pull/824/head
Morgan Pretty 1 year ago
commit a21839536c

@ -413,7 +413,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
if shouldRestartCamera { cameraManager.prepare() } if shouldRestartCamera { cameraManager.prepare() }
touch(call.videoCapturer) _ = call.videoCapturer // Force the lazy var to instantiate
titleLabel.text = self.call.contactName titleLabel.text = self.call.contactName
AppEnvironment.shared.callManager.startCall(call) { [weak self] error in AppEnvironment.shared.callManager.startCall(call) { [weak self] error in
DispatchQueue.main.async { DispatchQueue.main.async {
@ -468,7 +468,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
view.addSubview(titleLabel) view.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.center(.vertical, in: minimizeButton) titleLabel.center(.vertical, in: minimizeButton)
titleLabel.center(.horizontal, in: view) titleLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing)
titleLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing)
// Response Panel // Response Panel
view.addSubview(responsePanel) view.addSubview(responsePanel)

@ -661,6 +661,10 @@ extension ConversationVC:
} }
func inputTextViewDidChangeContent(_ inputTextView: InputTextView) { func inputTextViewDidChangeContent(_ inputTextView: InputTextView) {
// Note: If there is a 'draft' message then we don't want it to trigger the typing indicator to
// appear (as that is not expected/correct behaviour)
guard !viewIsAppearing else { return }
let newText: String = (inputTextView.text ?? "") let newText: String = (inputTextView.text ?? "")
if !newText.isEmpty { if !newText.isEmpty {

@ -441,11 +441,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
// Flag that the initial layout has been completed (the flag blocks and unblocks a number
// of different behaviours)
didFinishInitialLayout = true
viewIsAppearing = false
if delayFirstResponder || isShowingSearchUI { if delayFirstResponder || isShowingSearchUI {
delayFirstResponder = false delayFirstResponder = false
@ -457,7 +452,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
} }
} }
recoverInputView() recoverInputView { [weak self] in
// Flag that the initial layout has been completed (the flag blocks and unblocks a number
// of different behaviours)
self?.didFinishInitialLayout = true
self?.viewIsAppearing = false
}
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
@ -1261,7 +1261,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
self.blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self.view) self.blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self.view)
} }
func recoverInputView() { func recoverInputView(completion: (() -> ())? = nil) {
// This is a workaround for an issue where the textview is not scrollable // This is a workaround for an issue where the textview is not scrollable
// after the app goes into background and goes back in foreground. // after the app goes into background and goes back in foreground.
DispatchQueue.main.async { DispatchQueue.main.async {

@ -6,6 +6,7 @@
#import "OWSMath.h" #import "OWSMath.h"
#import "UIView+OWS.h" #import "UIView+OWS.h"
#import <QuartzCore/QuartzCore.h> #import <QuartzCore/QuartzCore.h>
#import <PureLayout/PureLayout.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> #import <SessionSnodeKit/SessionSnodeKit.h>

@ -40,7 +40,7 @@ final class IP2Country {
private func cacheCountry(for ip: String) -> String { private func cacheCountry(for ip: String) -> String {
if let result = countryNamesCache[ip] { return result } if let result = countryNamesCache[ip] { return result }
let ipAsInt = IPv4.toInt(ip) let ipAsInt = IPv4.toInt(ip)
guard let ipv4TableIndex = given(ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }), { $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted guard let ipv4TableIndex = ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted
let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex] let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex]
guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" } guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" }
let result = countryNamesTable["country_name"]![countryNamesTableIndex] let result = countryNamesTable["country_name"]![countryNamesTableIndex]

@ -493,6 +493,7 @@ extension Attachment {
public let interactionId: Int64 public let interactionId: Int64
public let state: Attachment.State public let state: Attachment.State
public let downloadUrl: String? public let downloadUrl: String?
public let albumIndex: Int
} }
public static func stateInfo(authorId: String, state: State? = nil) -> SQLRequest<Attachment.StateInfo> { public static func stateInfo(authorId: String, state: State? = nil) -> SQLRequest<Attachment.StateInfo> {
@ -510,7 +511,8 @@ extension Attachment {
\(attachment[.id]) AS attachmentId, \(attachment[.id]) AS attachmentId,
\(interaction[.id]) AS interactionId, \(interaction[.id]) AS interactionId,
\(attachment[.state]) AS state, \(attachment[.state]) AS state,
\(attachment[.downloadUrl]) AS downloadUrl \(attachment[.downloadUrl]) AS downloadUrl,
IFNULL(\(interactionAttachment[.albumIndex]), 0) AS albumIndex
FROM \(Attachment.self) FROM \(Attachment.self)
@ -520,8 +522,7 @@ extension Attachment {
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
( (
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
/* Note: This equation MUST match the `linkPreviewFilterLiteral` logic in Interaction.swift */ \(Interaction.linkPreviewFilterLiteral)
(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])
) )
) )
@ -556,7 +557,8 @@ extension Attachment {
\(attachment[.id]) AS attachmentId, \(attachment[.id]) AS attachmentId,
\(interaction[.id]) AS interactionId, \(interaction[.id]) AS interactionId,
\(attachment[.state]) AS state, \(attachment[.state]) AS state,
\(attachment[.downloadUrl]) AS downloadUrl \(attachment[.downloadUrl]) AS downloadUrl,
IFNULL(\(interactionAttachment[.albumIndex]), 0) AS albumIndex
FROM \(Attachment.self) FROM \(Attachment.self)
@ -566,8 +568,7 @@ extension Attachment {
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR \(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
( (
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
/* Note: This equation MUST match the `linkPreviewFilterLiteral` logic in Interaction.swift */ \(Interaction.linkPreviewFilterLiteral)
(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])
) )
) )

@ -29,13 +29,13 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
/// Whenever using this `linkPreview` association make sure to filter the result using /// Whenever using this `linkPreview` association make sure to filter the result using
/// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned /// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned
public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey) public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
public static func linkPreviewFilterLiteral( public static var linkPreviewFilterLiteral: SQL = {
timestampColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
) -> SQL {
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias() let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
let halfResolution: Double = LinkPreview.timstampResolution
return "(ROUND((\(Interaction.self).\(timestampColumn) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])"
} return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) AND (\(linkPreview[.timestamp]) + \(halfResolution)))"
}()
public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey) public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
public typealias Columns = CodingKeys public typealias Columns = CodingKeys
@ -246,10 +246,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
public var linkPreview: QueryInterfaceRequest<LinkPreview> { public var linkPreview: QueryInterfaceRequest<LinkPreview> {
/// **Note:** This equation **MUST** match the `linkPreviewFilterLiteral` logic /// **Note:** This equation **MUST** match the `linkPreviewFilterLiteral` logic
let halfResolution: Double = LinkPreview.timstampResolution
let roundedTimestamp: Double = (round(((Double(timestampMs) / 1000) / 100000) - 0.5) * 100000) let roundedTimestamp: Double = (round(((Double(timestampMs) / 1000) / 100000) - 0.5) * 100000)
return request(for: Interaction.linkPreview) return request(for: Interaction.linkPreview)
.filter(LinkPreview.Columns.timestamp == roundedTimestamp) .filter(
(Interaction.Columns.timestampMs >= (LinkPreview.Columns.timestamp - halfResolution)) &&
(Interaction.Columns.timestampMs <= (LinkPreview.Columns.timestamp + halfResolution))
)
} }
public var recipientStates: QueryInterfaceRequest<RecipientState> { public var recipientStates: QueryInterfaceRequest<RecipientState> {

@ -19,8 +19,9 @@ public final class FileServerAPI: NSObject {
/// exactly will be fine but a single byte more will result in an error /// exactly will be fine but a single byte more will result in an error
public static let maxFileSize = 10_000_000 public static let maxFileSize = 10_000_000
/// Standard timeout is 10 seconds which is a little too short fir file upload/download with slightly larger files /// Standard timeout is 10 seconds which is a little too short for file upload/download with slightly larger files
public static let fileTimeout: TimeInterval = 30 public static let fileDownloadTimeout: TimeInterval = 30
public static let fileUploadTimeout: TimeInterval = 60
// MARK: - File Storage // MARK: - File Storage
@ -36,7 +37,7 @@ public final class FileServerAPI: NSObject {
body: Array(file) body: Array(file)
) )
return send(request, serverPublicKey: serverPublicKey) return send(request, serverPublicKey: serverPublicKey, timeout: FileServerAPI.fileUploadTimeout)
.decoded(as: FileUploadResponse.self, on: .global(qos: .userInitiated)) .decoded(as: FileUploadResponse.self, on: .global(qos: .userInitiated))
} }
@ -47,7 +48,7 @@ public final class FileServerAPI: NSObject {
endpoint: .fileIndividual(fileId: fileId) endpoint: .fileIndividual(fileId: fileId)
) )
return send(request, serverPublicKey: serverPublicKey) return send(request, serverPublicKey: serverPublicKey, timeout: FileServerAPI.fileDownloadTimeout)
} }
public static func getVersion(_ platform: String) -> Promise<String> { public static func getVersion(_ platform: String) -> Promise<String> {
@ -59,14 +60,18 @@ public final class FileServerAPI: NSObject {
] ]
) )
return send(request, serverPublicKey: serverPublicKey) return send(request, serverPublicKey: serverPublicKey, timeout: HTTP.timeout)
.decoded(as: VersionResponse.self, on: .global(qos: .userInitiated)) .decoded(as: VersionResponse.self, on: .global(qos: .userInitiated))
.map { response in response.version } .map { response in response.version }
} }
// MARK: - Convenience // MARK: - Convenience
private static func send<T: Encodable>(_ request: Request<T, Endpoint>, serverPublicKey: String) -> Promise<Data> { private static func send<T: Encodable>(
_ request: Request<T, Endpoint>,
serverPublicKey: String,
timeout: TimeInterval
) -> Promise<Data> {
let urlRequest: URLRequest let urlRequest: URLRequest
do { do {
@ -76,7 +81,13 @@ public final class FileServerAPI: NSObject {
return Promise(error: error) return Promise(error: error)
} }
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey, timeout: FileServerAPI.fileTimeout) return OnionRequestAPI
.sendOnionRequest(
urlRequest,
to: request.server,
with: serverPublicKey,
timeout: timeout
)
.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 }

@ -141,7 +141,7 @@ public enum GarbageCollectionJob: JobExecutor {
FROM \(LinkPreview.self) FROM \(LinkPreview.self)
LEFT JOIN \(Interaction.self) ON ( LEFT JOIN \(Interaction.self) ON (
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
\(Interaction.linkPreviewFilterLiteral()) \(Interaction.linkPreviewFilterLiteral)
) )
WHERE \(interaction[.id]) IS NULL WHERE \(interaction[.id]) IS NULL
) )

@ -57,6 +57,7 @@ public enum MessageSendJob: JobExecutor {
.stateInfo(interactionId: interactionId) .stateInfo(interactionId: interactionId)
.fetchAll(db) .fetchAll(db)
let maybeFileIds: [String?] = allAttachmentStateInfo let maybeFileIds: [String?] = allAttachmentStateInfo
.sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex }
.map { Attachment.fileId(for: $0.downloadUrl) } .map { Attachment.fileId(for: $0.downloadUrl) }
let fileIds: [String] = maybeFileIds.compactMap { $0 } let fileIds: [String] = maybeFileIds.compactMap { $0 }

@ -160,14 +160,21 @@ public final class VisibleMessage: Message {
// Attachments // Attachments
let attachments: [Attachment]? = try? Attachment.fetchAll(db, ids: self.attachmentIds) let attachmentIdIndexes: [String: Int] = (try? InteractionAttachment
.filter(self.attachmentIds.contains(InteractionAttachment.Columns.attachmentId))
.fetchAll(db))
.defaulting(to: [])
.reduce(into: [:]) { result, next in result[next.attachmentId] = next.albumIndex }
let attachments: [Attachment] = (try? Attachment.fetchAll(db, ids: self.attachmentIds))
.defaulting(to: [])
.sorted { lhs, rhs in (attachmentIdIndexes[lhs.id] ?? 0) < (attachmentIdIndexes[rhs.id] ?? 0) }
if !(attachments ?? []).allSatisfy({ $0.state == .uploaded }) { if !attachments.allSatisfy({ $0.state == .uploaded }) {
#if DEBUG #if DEBUG
preconditionFailure("Sending a message before all associated attachments have been uploaded.") preconditionFailure("Sending a message before all associated attachments have been uploaded.")
#endif #endif
} }
let attachmentProtos = (attachments ?? []).compactMap { $0.buildProto() } let attachmentProtos = attachments.compactMap { $0.buildProto() }
dataMessage.setAttachments(attachmentProtos) dataMessage.setAttachments(attachmentProtos)
// Open group invitation // Open group invitation

@ -871,7 +871,7 @@ public enum OpenGroupAPI {
], ],
body: bytes body: bytes
), ),
timeout: FileServerAPI.fileTimeout, timeout: FileServerAPI.fileUploadTimeout,
using: dependencies using: dependencies
) )
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) .decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
@ -891,7 +891,7 @@ public enum OpenGroupAPI {
server: server, server: server,
endpoint: .roomFileIndividual(roomToken, fileId) endpoint: .roomFileIndividual(roomToken, fileId)
), ),
timeout: FileServerAPI.fileTimeout, timeout: FileServerAPI.fileDownloadTimeout,
using: dependencies using: dependencies
) )
.map { responseInfo, maybeData in .map { responseInfo, maybeData in

@ -1083,7 +1083,11 @@ public final class OpenGroupManager: NSObject {
} }
public static func parseOpenGroup(from string: String) -> (room: String, server: String, publicKey: String)? { public static func parseOpenGroup(from string: String) -> (room: String, server: String, publicKey: String)? {
guard let url = URL(string: string), let host = url.host ?? given(string.split(separator: "/").first, { String($0) }), let query = url.query else { return nil } guard
let url = URL(string: string),
let host = (url.host ?? string.split(separator: "/").first.map({ String($0) })),
let query = url.query
else { return nil }
// Inputs that should work: // Inputs that should work:
// https://sessionopengroup.co/r/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c // https://sessionopengroup.co/r/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c
// https://sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c // https://sessionopengroup.co/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231c

@ -327,10 +327,9 @@ public enum MessageReceiver {
if let name = name, !name.isEmpty, 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 = UserDefaults.standard[.lastDisplayNameUpdate]
sentTimestamp > $0.timeIntervalSince1970 .map { sentTimestamp > $0.timeIntervalSince1970 }
} .defaulting(to: true)
.defaulting(to: true)
} }
else { else {
shouldUpdate = true shouldUpdate = true
@ -354,10 +353,9 @@ public enum MessageReceiver {
{ {
let shouldUpdate: Bool let shouldUpdate: Bool
if isCurrentUser { if isCurrentUser {
shouldUpdate = given(UserDefaults.standard[.lastProfilePictureUpdate]) { shouldUpdate = UserDefaults.standard[.lastProfilePictureUpdate]
sentTimestamp > $0.timeIntervalSince1970 .map { sentTimestamp > $0.timeIntervalSince1970 }
} .defaulting(to: true)
.defaulting(to: true)
} }
else { else {
shouldUpdate = true shouldUpdate = true

@ -637,27 +637,33 @@ public extension MessageViewModel {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias() let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias() let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias() let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias() let profile: TypedTableAlias<Profile> = TypedTableAlias()
let quote: TypedTableAlias<Quote> = TypedTableAlias() let quote: TypedTableAlias<Quote> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias() let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
let threadProfileTableLiteral: SQL = SQL(stringLiteral: "threadProfile") let threadProfile: SQL = SQL(stringLiteral: "threadProfile")
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) let quoteInteraction: SQL = SQL(stringLiteral: "quoteInteraction")
let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) let quoteInteractionAttachment: SQL = SQL(stringLiteral: "quoteInteractionAttachment")
let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) let readReceipt: SQL = SQL(stringLiteral: "readReceipt")
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) let idColumn: SQL = SQL(stringLiteral: Interaction.Columns.id.name)
let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt") let interactionBodyColumn: SQL = SQL(stringLiteral: Interaction.Columns.body.name)
let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) let profileIdColumn: SQL = SQL(stringLiteral: Profile.Columns.id.name)
let attachmentIdColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.id.name) let nicknameColumn: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
let groupMemberModeratorTableLiteral: SQL = SQL(stringLiteral: "groupMemberModerator") let nameColumn: SQL = SQL(stringLiteral: Profile.Columns.name.name)
let groupMemberAdminTableLiteral: SQL = SQL(stringLiteral: "groupMemberAdmin") let quoteBodyColumn: SQL = SQL(stringLiteral: Quote.Columns.body.name)
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) let quoteAttachmentIdColumn: SQL = SQL(stringLiteral: Quote.Columns.attachmentId.name)
let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name) let readReceiptInteractionIdColumn: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name) let readTimestampMsColumn: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name)
let timestampMsColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let authorIdColumn: SQL = SQL(stringLiteral: Interaction.Columns.authorId.name)
let attachmentIdColumn: SQL = SQL(stringLiteral: Attachment.Columns.id.name)
let interactionAttachmentInteractionIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name)
let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name)
let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name)
let numColumnsBeforeLinkedRecords: Int = 20 let numColumnsBeforeLinkedRecords: Int = 20
let finalGroupSQL: SQL = (groupSQL ?? "") let finalGroupSQL: SQL = (groupSQL ?? "")
@ -671,7 +677,7 @@ public extension MessageViewModel {
IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey), IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey),
\(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey), \(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey),
\(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey), \(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey),
IFNULL(\(threadProfileTableLiteral).\(profileNicknameColumnLiteral), \(threadProfileTableLiteral).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey), IFNULL(\(threadProfile).\(nicknameColumn), \(threadProfile).\(nameColumn)) AS \(ViewModel.threadContactNameInternalKey),
\(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey),
\(interaction[.id]), \(interaction[.id]),
@ -685,20 +691,30 @@ public extension MessageViewModel {
-- Default to 'sending' assuming non-processed interaction when null -- Default to 'sending' assuming non-processed interaction when null
IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey), IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey),
(\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey), (\(readReceipt).\(readTimestampMsColumn) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey),
\(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey), \(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey),
( EXISTS (
\(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) IS NOT NULL OR SELECT 1
\(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) IS NOT NULL FROM \(GroupMember.self)
WHERE (
\(groupMember[.groupId]) = \(interaction[.threadId]) AND
\(groupMember[.profileId]) = \(interaction[.authorId]) AND
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
\(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])"))
)
) AS \(ViewModel.isSenderOpenGroupModeratorKey), ) AS \(ViewModel.isSenderOpenGroupModeratorKey),
\(ViewModel.profileKey).*, \(ViewModel.profileKey).*,
\(ViewModel.quoteKey).*, \(quote[.interactionId]),
\(quote[.authorId]),
\(quote[.timestampMs]),
\(quoteInteraction).\(interactionBodyColumn) AS \(quoteBodyColumn),
\(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn),
\(ViewModel.quoteAttachmentKey).*, \(ViewModel.quoteAttachmentKey).*,
\(ViewModel.linkPreviewKey).*, \(ViewModel.linkPreviewKey).*,
\(ViewModel.linkPreviewAttachmentKey).*, \(ViewModel.linkPreviewAttachmentKey).*,
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey), \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey),
-- All of the below properties are set in post-query processing but to prevent the -- All of the below properties are set in post-query processing but to prevent the
@ -715,54 +731,40 @@ public extension MessageViewModel {
FROM \(Interaction.self) FROM \(Interaction.self)
JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId])
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(interaction[.threadId]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(interaction[.threadId])
LEFT JOIN \(Profile.self) AS \(threadProfileTableLiteral) ON \(threadProfileTableLiteral).\(profileIdColumnLiteral) = \(interaction[.threadId]) LEFT JOIN \(Profile.self) AS \(threadProfile) ON \(threadProfile).\(profileIdColumn) = \(interaction[.threadId])
LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId])
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
LEFT JOIN ( LEFT JOIN \(Quote.self) ON \(quote[.interactionId]) = \(interaction[.id])
SELECT \(quote[.interactionId]), LEFT JOIN \(Interaction.self) AS \(quoteInteraction) ON (
\(quote[.authorId]), \(quoteInteraction).\(timestampMsColumn) = \(quote[.timestampMs]) AND (
\(quote[.timestampMs]), \(quoteInteraction).\(authorIdColumn) = \(quote[.authorId]) OR (
\(interaction[.body]) AS \(Quote.Columns.body), -- A users outgoing message is stored in some cases using their standard id
\(interactionAttachment[.attachmentId]) AS \(Quote.Columns.attachmentId) -- but the quote will use their blinded id so handle that case
FROM \(Quote.self) \(quote[.authorId]) = \(blindedPublicKey ?? "''") AND
LEFT JOIN \(Interaction.self) ON ( \(quoteInteraction).\(authorIdColumn) = \(userPublicKey)
( )
\(quote[.authorId]) = \(interaction[.authorId]) OR (
\(quote[.authorId]) = \(blindedPublicKey ?? "") AND
\(userPublicKey) = \(interaction[.authorId])
)
) AND
\(quote[.timestampMs]) = \(interaction[.timestampMs])
) )
LEFT JOIN \(InteractionAttachment.self) ON \(interaction[.id]) = \(interactionAttachment[.interactionId]) )
) AS \(ViewModel.quoteKey) ON \(quote[.interactionId]) = \(interaction[.id]) LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON (
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumnLiteral) = \(quote[.attachmentId]) \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction).\(idColumn) AND
\(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0
)
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn)
LEFT JOIN \(LinkPreview.self) ON ( LEFT JOIN \(LinkPreview.self) ON (
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
\(Interaction.linkPreviewFilterLiteral()) \(Interaction.linkPreviewFilterLiteral)
) )
LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumnLiteral) = \(linkPreview[.attachmentId]) LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumn) = \(linkPreview[.attachmentId])
LEFT JOIN \(RecipientState.self) ON ( LEFT JOIN \(RecipientState.self) ON (
-- Ignore 'skipped' states -- Ignore 'skipped' states
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
\(recipientState[.interactionId]) = \(interaction[.id]) \(recipientState[.interactionId]) = \(interaction[.id])
) )
LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON ( LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON (
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND \(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) \(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id])
)
LEFT JOIN \(GroupMember.self) AS \(groupMemberModeratorTableLiteral) ON (
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
\(groupMemberModeratorTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND
\(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
\(SQL("\(groupMemberModeratorTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.moderator)"))
)
LEFT JOIN \(GroupMember.self) AS \(groupMemberAdminTableLiteral) ON (
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
\(groupMemberAdminTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND
\(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
\(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)"))
) )
WHERE \(interaction.alias[Column.rowID]) IN \(rowIds) WHERE \(interaction.alias[Column.rowID]) IN \(rowIds)
\(finalGroupSQL) \(finalGroupSQL)

@ -448,7 +448,8 @@ public extension SessionThreadViewModel {
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias() let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias() let profile: TypedTableAlias<Profile> = TypedTableAlias()
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction")
let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt") let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt")
let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name)
@ -459,9 +460,7 @@ public extension SessionThreadViewModel {
let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name)
let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name)
let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name)
let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name)
let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name)
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
/// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before
/// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to
@ -470,124 +469,136 @@ public extension SessionThreadViewModel {
/// Explicitly set default values for the fields ignored for search results /// Explicitly set default values for the fields ignored for search results
let numColumnsBeforeProfiles: Int = 12 let numColumnsBeforeProfiles: Int = 12
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined
let request: SQLRequest<ViewModel> = """ let request: SQLRequest<ViewModel> = """
SELECT SELECT
\(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey),
\(thread[.id]) AS \(ViewModel.threadIdKey), \(thread[.id]) AS \(ViewModel.threadIdKey),
\(thread[.variant]) AS \(ViewModel.threadVariantKey), \(thread[.variant]) AS \(ViewModel.threadVariantKey),
\(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey),
(\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey),
\(thread[.isPinned]) AS \(ViewModel.threadIsPinnedKey), \(thread[.isPinned]) AS \(ViewModel.threadIsPinnedKey),
\(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey),
\(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey),
\(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey),
(\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey), (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey),
\(Interaction.self).\(ViewModel.threadUnreadCountKey), \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey),
\(Interaction.self).\(ViewModel.threadUnreadMentionCountKey), \(aggregateInteractionLiteral).\(ViewModel.threadUnreadMentionCountKey),
\(ViewModel.contactProfileKey).*, \(ViewModel.contactProfileKey).*,
\(ViewModel.closedGroupProfileFrontKey).*, \(ViewModel.closedGroupProfileFrontKey).*,
\(ViewModel.closedGroupProfileBackKey).*, \(ViewModel.closedGroupProfileBackKey).*,
\(ViewModel.closedGroupProfileBackFallbackKey).*, \(ViewModel.closedGroupProfileBackFallbackKey).*,
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
(\(ViewModel.currentUserIsClosedGroupMemberKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
(\(ViewModel.currentUserIsClosedGroupAdminKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey), EXISTS (
SELECT 1
FROM \(GroupMember.self)
WHERE (
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
)
) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
EXISTS (
SELECT 1
FROM \(GroupMember.self)
WHERE (
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
)
) AS \(ViewModel.currentUserIsClosedGroupAdminKey),
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey), \(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
\(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey),
\(Interaction.self).\(ViewModel.interactionIdKey), \(interaction[.id]) AS \(ViewModel.interactionIdKey),
\(Interaction.self).\(ViewModel.interactionVariantKey), \(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
\(Interaction.self).\(interactionTimestampMsColumnLiteral) AS \(ViewModel.interactionTimestampMsKey), \(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey),
\(Interaction.self).\(ViewModel.interactionBodyKey), \(interaction[.body]) AS \(ViewModel.interactionBodyKey),
-- Default to 'sending' assuming non-processed interaction when null -- Default to 'sending' assuming non-processed interaction when null
IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.interactionStateKey), IFNULL((
SELECT \(recipientState[.state])
FROM \(RecipientState.self)
WHERE (
\(recipientState[.interactionId]) = \(interaction[.id]) AND
-- Ignore 'skipped' states
\(SQL("\(recipientState[.state]) = \(RecipientState.State.sending)"))
)
LIMIT 1
), 0) AS \(ViewModel.interactionStateKey),
(\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.interactionHasAtLeastOneReadReceiptKey), (\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.interactionHasAtLeastOneReadReceiptKey),
(\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey), (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey),
-- These 4 properties will be combined into 'Attachment.DescriptionInfo' -- These 4 properties will be combined into 'Attachment.DescriptionInfo'
\(attachment[.id]), \(attachment[.id]),
\(attachment[.variant]), \(attachment[.variant]),
\(attachment[.contentType]), \(attachment[.contentType]),
\(attachment[.sourceFilename]), \(attachment[.sourceFilename]),
COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey), COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey),
\(interaction[.authorId]), \(interaction[.authorId]),
IFNULL(\(ViewModel.contactProfileKey).\(profileNicknameColumnLiteral), \(ViewModel.contactProfileKey).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey), IFNULL(\(ViewModel.contactProfileKey).\(profileNicknameColumnLiteral), \(ViewModel.contactProfileKey).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey),
IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey),
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
FROM \(SessionThread.self) FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id]) LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id])
LEFT JOIN ( LEFT JOIN (
-- Fetch all interaction-specific data in a subquery to be more efficient
SELECT SELECT
\(interaction[.id]) AS \(ViewModel.interactionIdKey), \(interaction[.id]) AS \(ViewModel.interactionIdKey),
\(interaction[.threadId]), \(interaction[.threadId]) AS \(ViewModel.threadIdKey),
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey), MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral),
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral),
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
\(interaction[.authorId]),
\(interaction[.linkPreviewUrl]),
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey), SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey),
SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey) SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey)
FROM \(Interaction.self) FROM \(Interaction.self)
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
GROUP BY \(interaction[.threadId]) GROUP BY \(interaction[.threadId])
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id])
LEFT JOIN \(RecipientState.self) ON ( LEFT JOIN \(Interaction.self) ON (
-- Ignore 'skipped' states \(interaction[.threadId]) = \(thread[.id]) AND
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND \(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey)
\(recipientState[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey)
) )
LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON ( LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON (
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND \(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) AND
\(Interaction.self).\(ViewModel.interactionIdKey) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) \(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL
) )
LEFT JOIN \(LinkPreview.self) ON ( LEFT JOIN \(LinkPreview.self) ON (
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND \(Interaction.linkPreviewFilterLiteral) AND
\(Interaction.linkPreviewFilterLiteral(timestampColumn: interactionTimestampMsColumnLiteral)) \(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)"))
) )
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON ( LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND \(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id]) AND
\(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(Interaction.self).\(ViewModel.interactionIdKey) \(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0
) )
LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral) LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral)
LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey) LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(interaction[.id])
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
-- Thread naming & avatar content -- Thread naming & avatar content
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupMemberKey) ON (
\(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberRoleColumnLiteral) != \(GroupMember.Role.zombie)")) AND
\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND
\(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)"))
)
LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupAdminKey) ON (
\(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)")) AND
\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND
\(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)"))
)
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
SELECT MIN(\(groupMember[.profileId])) SELECT MIN(\(groupMember[.profileId]))
FROM \(GroupMember.self) FROM \(GroupMember.self)
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
WHERE ( WHERE (
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(SQL("\(groupMember[.profileId]) != \(userPublicKey)")) \(SQL("\(groupMember[.profileId]) != \(userPublicKey)"))
) )
) )
@ -599,8 +610,8 @@ public extension SessionThreadViewModel {
FROM \(GroupMember.self) FROM \(GroupMember.self)
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
WHERE ( WHERE (
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(SQL("\(groupMember[.profileId]) != \(userPublicKey)")) \(SQL("\(groupMember[.profileId]) != \(userPublicKey)"))
) )
) )
@ -610,7 +621,7 @@ public extension SessionThreadViewModel {
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)"))
) )
WHERE \(thread.alias[Column.rowID]) IN \(rowIds) WHERE \(thread.alias[Column.rowID]) IN \(rowIds)
\(groupSQL) \(groupSQL)
ORDER BY \(orderSQL) ORDER BY \(orderSQL)
@ -643,14 +654,14 @@ public extension SessionThreadViewModel {
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
return """ return """
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
\(interaction[.threadId]), \(interaction[.threadId]),
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral) MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral)
FROM \(Interaction.self) FROM \(Interaction.self)
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
GROUP BY \(interaction[.threadId]) GROUP BY \(interaction[.threadId])
@ -701,7 +712,10 @@ public extension SessionThreadViewModel {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
return SQL("\(thread[.isPinned]) DESC, IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC") return SQL("""
\(thread[.isPinned]) DESC,
CASE WHEN \(interaction[.timestampMs]) IS NOT NULL THEN \(interaction[.timestampMs]) ELSE (\(thread[.creationDateTimestamp]) * 1000) END DESC
""")
}() }()
static let messageRequetsOrderSQL: SQL = { static let messageRequetsOrderSQL: SQL = {
@ -725,6 +739,8 @@ public extension SessionThreadViewModel {
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias() let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction")
let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table") let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table")
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
@ -760,12 +776,22 @@ public extension SessionThreadViewModel {
\(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey),
\(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey), \(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey),
\(Interaction.self).\(ViewModel.threadUnreadCountKey), \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey),
\(ViewModel.contactProfileKey).*, \(ViewModel.contactProfileKey).*,
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
\(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey), \(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey),
(\(groupMember[.profileId]) IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
EXISTS (
SELECT 1
FROM \(GroupMember.self)
WHERE (
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
)
) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey), \(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
\(openGroup[.server]) AS \(ViewModel.openGroupServerKey), \(openGroup[.server]) AS \(ViewModel.openGroupServerKey),
\(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey),
@ -773,33 +799,28 @@ public extension SessionThreadViewModel {
\(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey), \(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey),
\(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey), \(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey),
\(Interaction.self).\(ViewModel.interactionIdKey), \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey),
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
FROM \(SessionThread.self) FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN ( LEFT JOIN (
-- Fetch all interaction-specific data in a subquery to be more efficient
SELECT SELECT
\(interaction[.id]) AS \(ViewModel.interactionIdKey), \(interaction[.id]) AS \(ViewModel.interactionIdKey),
\(interaction[.threadId]), \(interaction[.threadId]) AS \(ViewModel.threadIdKey),
MAX(\(interaction[.timestampMs])), MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral),
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey) SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey)
FROM \(Interaction.self) FROM \(Interaction.self)
WHERE \(SQL("\(interaction[.threadId]) = \(threadId)")) WHERE (
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) \(SQL("\(interaction[.threadId]) = \(threadId)")) AND
\(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
)
) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id])
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(GroupMember.self) ON (
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
)
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
\(groupMember[.groupId]), \(groupMember[.groupId]),
@ -1583,7 +1604,7 @@ public extension SessionThreadViewModel {
FROM \(SessionThread.self) FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN ( LEFT JOIN (
SELECT *, MAX(\(interaction[.timestampMs])) SELECT \(interaction[.threadId]), MAX(\(interaction[.timestampMs]))
FROM \(Interaction.self) FROM \(Interaction.self)
GROUP BY \(interaction[.threadId]) GROUP BY \(interaction[.threadId])
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) ) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])

@ -308,9 +308,16 @@ public enum Preferences {
} }
public static var isCallKitSupported: Bool { public static var isCallKitSupported: Bool {
#if targetEnvironment(simulator)
/// The iOS simulator doesn't support CallKit, when receiving a call on the simulator and routing it via CallKit it
/// will immediately trigger a hangup making it difficult to test - instead we just should just avoid using CallKit
/// entirely on the simulator
return false
#else
guard let regionCode: String = NSLocale.current.regionCode else { return false } guard let regionCode: String = NSLocale.current.regionCode else { return false }
guard !regionCode.contains("CN") && !regionCode.contains("CHN") else { return false } guard !regionCode.contains("CN") && !regionCode.contains("CHN") else { return false }
return true return true
#endif
} }
} }

@ -312,9 +312,9 @@ public final class SnodeAPI {
public static func getSnodePool() -> Promise<Set<Snode>> { public static func getSnodePool() -> Promise<Set<Snode>> {
loadSnodePoolIfNeeded() loadSnodePoolIfNeeded()
let now = Date() let now = Date()
let hasSnodePoolExpired = given(Storage.shared[.lastSnodePoolRefreshDate]) { let hasSnodePoolExpired: Bool = Storage.shared[.lastSnodePoolRefreshDate]
now.timeIntervalSince($0) > 2 * 60 * 60 .map { now.timeIntervalSince($0) > 2 * 60 * 60 }
}.defaulting(to: true) .defaulting(to: true)
let snodePool: Set<Snode> = SnodeAPI.snodePool.wrappedValue let snodePool: Set<Snode> = SnodeAPI.snodePool.wrappedValue
guard hasInsufficientSnodes || hasSnodePoolExpired else { guard hasInsufficientSnodes || hasSnodePoolExpired else {

@ -17,13 +17,13 @@ public extension UIView {
class func spacer(withWidth width: CGFloat) -> UIView { class func spacer(withWidth width: CGFloat) -> UIView {
let view = UIView() let view = UIView()
view.autoSetDimension(.width, toSize: width) view.set(.width, to: width)
return view return view
} }
class func spacer(withHeight height: CGFloat) -> UIView { class func spacer(withHeight height: CGFloat) -> UIView {
let view = UIView() let view = UIView()
view.autoSetDimension(.height, toSize: height) view.set(.height, to: height)
return view return view
} }

@ -35,14 +35,3 @@ public func getUserHexEncodedPublicKey(_ db: Database? = nil, dependencies: Depe
return "" return ""
} }
/// Does nothing, but is never inlined and thus evaluating its argument will never be optimized away.
///
/// Useful for forcing the instantiation of lazy properties like globals.
@inline(never)
public func touch<Value>(_ value: Value) { /* Do nothing */ }
/// Returns `f(x!)` if `x != nil`, or `nil` otherwise.
public func given<T, U>(_ x: T?, _ f: (T) throws -> U) rethrows -> U? { return try x.map(f) }
public func with<T, U>(_ x: T, _ f: (T) throws -> U) rethrows -> U { return try f(x) }

@ -2,7 +2,6 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
#import <PureLayout/PureLayout.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN

@ -5,6 +5,7 @@
#import "UIView+OWS.h" #import "UIView+OWS.h"
#import "OWSMath.h" #import "OWSMath.h"
#import <PureLayout/PureLayout.h>
#import <SessionUtilitiesKit/AppContext.h> #import <SessionUtilitiesKit/AppContext.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN

@ -591,12 +591,17 @@ private final class JobQueue {
} }
fileprivate func appDidBecomeActive(with jobs: [Job], canStart: Bool) { fileprivate func appDidBecomeActive(with jobs: [Job], canStart: Bool) {
let currentlyRunningJobIds: Set<Int64> = jobsCurrentlyRunning.wrappedValue
queue.mutate { queue in queue.mutate { queue in
// Avoid re-adding jobs to the queue that are already in it (this can // Avoid re-adding jobs to the queue that are already in it (this can
// happen if the user sends the app to the background before the 'onActive' // happen if the user sends the app to the background before the 'onActive'
// jobs and then brings it back to the foreground) // jobs and then brings it back to the foreground)
let jobsNotAlreadyInQueue: [Job] = jobs let jobsNotAlreadyInQueue: [Job] = jobs
.filter { job in !queue.contains(where: { $0.id == job.id }) } .filter { job in
!currentlyRunningJobIds.contains(job.id ?? -1) &&
!queue.contains(where: { $0.id == job.id })
}
queue.append(contentsOf: jobsNotAlreadyInQueue) queue.append(contentsOf: jobsNotAlreadyInQueue)
} }
@ -784,14 +789,20 @@ private final class JobQueue {
guard dependencyInfo.jobs.isEmpty else { guard dependencyInfo.jobs.isEmpty else {
SNLog("[JobRunner] \(queueContext) found job with \(dependencyInfo.jobs.count) dependencies, running those first") SNLog("[JobRunner] \(queueContext) found job with \(dependencyInfo.jobs.count) dependencies, running those first")
/// Remove all jobs this one is dependant on from the queue and re-insert them at the start of the queue /// Remove all jobs this one is dependant on that aren't currently running from the queue and re-insert them at the start
/// of the queue
/// ///
/// **Note:** We don't add the current job back the the queue because it should only be re-added if it's dependencies /// **Note:** We don't add the current job back the the queue because it should only be re-added if it's dependencies
/// are successfully completed /// are successfully completed
let currentlyRunningJobIds: [Int64] = Array(detailsForCurrentlyRunningJobs.wrappedValue.keys)
let dependencyJobsNotCurrentlyRunning: [Job] = dependencyInfo.jobs
.filter { job in !currentlyRunningJobIds.contains(job.id ?? -1) }
.sorted { lhs, rhs in (lhs.id ?? -1) < (rhs.id ?? -1) }
queue.mutate { queue in queue.mutate { queue in
queue = queue queue = queue
.filter { !dependencyInfo.jobs.contains($0) } .filter { !dependencyJobsNotCurrentlyRunning.contains($0) }
.inserting(contentsOf: Array(dependencyInfo.jobs), at: 0) .inserting(contentsOf: dependencyJobsNotCurrentlyRunning, at: 0)
} }
handleJobDeferred(nextJob) handleJobDeferred(nextJob)
return return
@ -960,17 +971,22 @@ private final class JobQueue {
default: break default: break
} }
/// Now that the job has been completed we want to insert any jobs that were dependant on it to the start of the queue (the /// Now that the job has been completed we want to insert any jobs that were dependant on it, that aren't already running
/// most likely case is that we want an entire job chain to be completed at the same time rather than being blocked by other /// to the start of the queue (the most likely case is that we want an entire job chain to be completed at the same time rather
/// unrelated jobs) /// than being blocked by other unrelated jobs)
/// ///
/// **Note:** If any of these `dependantJobs` have other dependencies then when they attempt to start they will be /// **Note:** If any of these `dependantJobs` have other dependencies then when they attempt to start they will be
/// removed from the queue, replaced by their dependencies /// removed from the queue, replaced by their dependencies
if !dependantJobs.isEmpty { if !dependantJobs.isEmpty {
let currentlyRunningJobIds: [Int64] = Array(detailsForCurrentlyRunningJobs.wrappedValue.keys)
let dependantJobsNotCurrentlyRunning: [Job] = dependantJobs
.filter { job in !currentlyRunningJobIds.contains(job.id ?? -1) }
.sorted { lhs, rhs in (lhs.id ?? -1) < (rhs.id ?? -1) }
queue.mutate { queue in queue.mutate { queue in
queue = queue queue = queue
.filter { !dependantJobs.contains($0) } .filter { !dependantJobsNotCurrentlyRunning.contains($0) }
.inserting(contentsOf: dependantJobs, at: 0) .inserting(contentsOf: dependantJobsNotCurrentlyRunning, at: 0)
} }
} }

@ -5,6 +5,7 @@
import Foundation import Foundation
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import PureLayout
// Coincides with Android's max text message length // Coincides with Android's max text message length
let kMaxMessageBodyCharacterCount = 2000 let kMaxMessageBodyCharacterCount = 2000

Loading…
Cancel
Save