Merge remote-tracking branch 'upstream/dev' into fix/new-certificates
This commit is contained in:
commit
a21839536c
|
@ -413,7 +413,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
|
||||
if shouldRestartCamera { cameraManager.prepare() }
|
||||
|
||||
touch(call.videoCapturer)
|
||||
_ = call.videoCapturer // Force the lazy var to instantiate
|
||||
titleLabel.text = self.call.contactName
|
||||
AppEnvironment.shared.callManager.startCall(call) { [weak self] error in
|
||||
DispatchQueue.main.async {
|
||||
|
@ -468,7 +468,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
view.addSubview(titleLabel)
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
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
|
||||
view.addSubview(responsePanel)
|
||||
|
|
|
@ -661,6 +661,10 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
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 ?? "")
|
||||
|
||||
if !newText.isEmpty {
|
||||
|
|
|
@ -441,11 +441,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
override func viewDidAppear(_ animated: Bool) {
|
||||
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 {
|
||||
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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
func recoverInputView() {
|
||||
func recoverInputView(completion: (() -> ())? = nil) {
|
||||
// This is a workaround for an issue where the textview is not scrollable
|
||||
// after the app goes into background and goes back in foreground.
|
||||
DispatchQueue.main.async {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#import "OWSMath.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
|
||||
#import <SessionSnodeKit/SessionSnodeKit.h>
|
||||
|
|
|
@ -40,7 +40,7 @@ final class IP2Country {
|
|||
private func cacheCountry(for ip: String) -> String {
|
||||
if let result = countryNamesCache[ip] { return result }
|
||||
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]
|
||||
guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" }
|
||||
let result = countryNamesTable["country_name"]![countryNamesTableIndex]
|
||||
|
|
|
@ -493,6 +493,7 @@ extension Attachment {
|
|||
public let interactionId: Int64
|
||||
public let state: Attachment.State
|
||||
public let downloadUrl: String?
|
||||
public let albumIndex: Int
|
||||
}
|
||||
|
||||
public static func stateInfo(authorId: String, state: State? = nil) -> SQLRequest<Attachment.StateInfo> {
|
||||
|
@ -510,7 +511,8 @@ extension Attachment {
|
|||
\(attachment[.id]) AS attachmentId,
|
||||
\(interaction[.id]) AS interactionId,
|
||||
\(attachment[.state]) AS state,
|
||||
\(attachment[.downloadUrl]) AS downloadUrl
|
||||
\(attachment[.downloadUrl]) AS downloadUrl,
|
||||
IFNULL(\(interactionAttachment[.albumIndex]), 0) AS albumIndex
|
||||
|
||||
FROM \(Attachment.self)
|
||||
|
||||
|
@ -520,8 +522,7 @@ extension Attachment {
|
|||
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
|
||||
(
|
||||
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
|
||||
/* Note: This equation MUST match the `linkPreviewFilterLiteral` logic in Interaction.swift */
|
||||
(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])
|
||||
\(Interaction.linkPreviewFilterLiteral)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -556,7 +557,8 @@ extension Attachment {
|
|||
\(attachment[.id]) AS attachmentId,
|
||||
\(interaction[.id]) AS interactionId,
|
||||
\(attachment[.state]) AS state,
|
||||
\(attachment[.downloadUrl]) AS downloadUrl
|
||||
\(attachment[.downloadUrl]) AS downloadUrl,
|
||||
IFNULL(\(interactionAttachment[.albumIndex]), 0) AS albumIndex
|
||||
|
||||
FROM \(Attachment.self)
|
||||
|
||||
|
@ -566,8 +568,7 @@ extension Attachment {
|
|||
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
|
||||
(
|
||||
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
|
||||
/* Note: This equation MUST match the `linkPreviewFilterLiteral` logic in Interaction.swift */
|
||||
(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])
|
||||
\(Interaction.linkPreviewFilterLiteral)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -29,13 +29,13 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
/// Whenever using this `linkPreview` association make sure to filter the result using
|
||||
/// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned
|
||||
public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
|
||||
public static func linkPreviewFilterLiteral(
|
||||
timestampColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
|
||||
) -> SQL {
|
||||
public static var linkPreviewFilterLiteral: SQL = {
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
|
||||
return "(ROUND((\(Interaction.self).\(timestampColumn) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])"
|
||||
}
|
||||
let halfResolution: Double = LinkPreview.timstampResolution
|
||||
|
||||
return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) AND (\(linkPreview[.timestamp]) + \(halfResolution)))"
|
||||
}()
|
||||
public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
|
@ -246,10 +246,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
|
||||
public var linkPreview: QueryInterfaceRequest<LinkPreview> {
|
||||
/// **Note:** This equation **MUST** match the `linkPreviewFilterLiteral` logic
|
||||
let halfResolution: Double = LinkPreview.timstampResolution
|
||||
let roundedTimestamp: Double = (round(((Double(timestampMs) / 1000) / 100000) - 0.5) * 100000)
|
||||
|
||||
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> {
|
||||
|
|
|
@ -19,8 +19,9 @@ public final class FileServerAPI: NSObject {
|
|||
/// exactly will be fine but a single byte more will result in an error
|
||||
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
|
||||
public static let fileTimeout: TimeInterval = 30
|
||||
/// Standard timeout is 10 seconds which is a little too short for file upload/download with slightly larger files
|
||||
public static let fileDownloadTimeout: TimeInterval = 30
|
||||
public static let fileUploadTimeout: TimeInterval = 60
|
||||
|
||||
// MARK: - File Storage
|
||||
|
||||
|
@ -36,7 +37,7 @@ public final class FileServerAPI: NSObject {
|
|||
body: Array(file)
|
||||
)
|
||||
|
||||
return send(request, serverPublicKey: serverPublicKey)
|
||||
return send(request, serverPublicKey: serverPublicKey, timeout: FileServerAPI.fileUploadTimeout)
|
||||
.decoded(as: FileUploadResponse.self, on: .global(qos: .userInitiated))
|
||||
}
|
||||
|
||||
|
@ -47,7 +48,7 @@ public final class FileServerAPI: NSObject {
|
|||
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> {
|
||||
|
@ -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))
|
||||
.map { response in response.version }
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
do {
|
||||
|
@ -76,7 +81,13 @@ public final class FileServerAPI: NSObject {
|
|||
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
|
||||
guard let response: Data = response else { throw HTTP.Error.parsingFailed }
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@ public enum GarbageCollectionJob: JobExecutor {
|
|||
FROM \(LinkPreview.self)
|
||||
LEFT JOIN \(Interaction.self) ON (
|
||||
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
|
||||
\(Interaction.linkPreviewFilterLiteral())
|
||||
\(Interaction.linkPreviewFilterLiteral)
|
||||
)
|
||||
WHERE \(interaction[.id]) IS NULL
|
||||
)
|
||||
|
|
|
@ -57,6 +57,7 @@ public enum MessageSendJob: JobExecutor {
|
|||
.stateInfo(interactionId: interactionId)
|
||||
.fetchAll(db)
|
||||
let maybeFileIds: [String?] = allAttachmentStateInfo
|
||||
.sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex }
|
||||
.map { Attachment.fileId(for: $0.downloadUrl) }
|
||||
let fileIds: [String] = maybeFileIds.compactMap { $0 }
|
||||
|
||||
|
|
|
@ -160,14 +160,21 @@ public final class VisibleMessage: Message {
|
|||
|
||||
// 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
|
||||
preconditionFailure("Sending a message before all associated attachments have been uploaded.")
|
||||
#endif
|
||||
}
|
||||
let attachmentProtos = (attachments ?? []).compactMap { $0.buildProto() }
|
||||
let attachmentProtos = attachments.compactMap { $0.buildProto() }
|
||||
dataMessage.setAttachments(attachmentProtos)
|
||||
|
||||
// Open group invitation
|
||||
|
|
|
@ -871,7 +871,7 @@ public enum OpenGroupAPI {
|
|||
],
|
||||
body: bytes
|
||||
),
|
||||
timeout: FileServerAPI.fileTimeout,
|
||||
timeout: FileServerAPI.fileUploadTimeout,
|
||||
using: dependencies
|
||||
)
|
||||
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
|
||||
|
@ -891,7 +891,7 @@ public enum OpenGroupAPI {
|
|||
server: server,
|
||||
endpoint: .roomFileIndividual(roomToken, fileId)
|
||||
),
|
||||
timeout: FileServerAPI.fileTimeout,
|
||||
timeout: FileServerAPI.fileDownloadTimeout,
|
||||
using: dependencies
|
||||
)
|
||||
.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)? {
|
||||
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:
|
||||
// https://sessionopengroup.co/r/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 {
|
||||
let shouldUpdate: Bool
|
||||
if isCurrentUser {
|
||||
shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) {
|
||||
sentTimestamp > $0.timeIntervalSince1970
|
||||
}
|
||||
.defaulting(to: true)
|
||||
shouldUpdate = UserDefaults.standard[.lastDisplayNameUpdate]
|
||||
.map { sentTimestamp > $0.timeIntervalSince1970 }
|
||||
.defaulting(to: true)
|
||||
}
|
||||
else {
|
||||
shouldUpdate = true
|
||||
|
@ -354,10 +353,9 @@ public enum MessageReceiver {
|
|||
{
|
||||
let shouldUpdate: Bool
|
||||
if isCurrentUser {
|
||||
shouldUpdate = given(UserDefaults.standard[.lastProfilePictureUpdate]) {
|
||||
sentTimestamp > $0.timeIntervalSince1970
|
||||
}
|
||||
.defaulting(to: true)
|
||||
shouldUpdate = UserDefaults.standard[.lastProfilePictureUpdate]
|
||||
.map { sentTimestamp > $0.timeIntervalSince1970 }
|
||||
.defaulting(to: true)
|
||||
}
|
||||
else {
|
||||
shouldUpdate = true
|
||||
|
|
|
@ -637,27 +637,33 @@ public extension MessageViewModel {
|
|||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
|
||||
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
|
||||
let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
|
||||
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
||||
let quote: TypedTableAlias<Quote> = TypedTableAlias()
|
||||
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
|
||||
let threadProfileTableLiteral: SQL = SQL(stringLiteral: "threadProfile")
|
||||
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
|
||||
let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
|
||||
let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name)
|
||||
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
|
||||
let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt")
|
||||
let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name)
|
||||
let attachmentIdColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.id.name)
|
||||
let groupMemberModeratorTableLiteral: SQL = SQL(stringLiteral: "groupMemberModerator")
|
||||
let groupMemberAdminTableLiteral: SQL = SQL(stringLiteral: "groupMemberAdmin")
|
||||
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
|
||||
let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name)
|
||||
let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name)
|
||||
let threadProfile: SQL = SQL(stringLiteral: "threadProfile")
|
||||
let quoteInteraction: SQL = SQL(stringLiteral: "quoteInteraction")
|
||||
let quoteInteractionAttachment: SQL = SQL(stringLiteral: "quoteInteractionAttachment")
|
||||
let readReceipt: SQL = SQL(stringLiteral: "readReceipt")
|
||||
let idColumn: SQL = SQL(stringLiteral: Interaction.Columns.id.name)
|
||||
let interactionBodyColumn: SQL = SQL(stringLiteral: Interaction.Columns.body.name)
|
||||
let profileIdColumn: SQL = SQL(stringLiteral: Profile.Columns.id.name)
|
||||
let nicknameColumn: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
|
||||
let nameColumn: SQL = SQL(stringLiteral: Profile.Columns.name.name)
|
||||
let quoteBodyColumn: SQL = SQL(stringLiteral: Quote.Columns.body.name)
|
||||
let quoteAttachmentIdColumn: SQL = SQL(stringLiteral: Quote.Columns.attachmentId.name)
|
||||
let readReceiptInteractionIdColumn: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.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 finalGroupSQL: SQL = (groupSQL ?? "")
|
||||
|
@ -671,7 +677,7 @@ public extension MessageViewModel {
|
|||
IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey),
|
||||
\(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey),
|
||||
\(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[.id]),
|
||||
|
@ -685,20 +691,30 @@ public extension MessageViewModel {
|
|||
|
||||
-- Default to 'sending' assuming non-processed interaction when null
|
||||
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),
|
||||
|
||||
(
|
||||
\(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) IS NOT NULL OR
|
||||
\(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) IS NOT NULL
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
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),
|
||||
|
||||
\(ViewModel.profileKey).*,
|
||||
\(ViewModel.quoteKey).*,
|
||||
\(quote[.interactionId]),
|
||||
\(quote[.authorId]),
|
||||
\(quote[.timestampMs]),
|
||||
\(quoteInteraction).\(interactionBodyColumn) AS \(quoteBodyColumn),
|
||||
\(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn),
|
||||
\(ViewModel.quoteAttachmentKey).*,
|
||||
\(ViewModel.linkPreviewKey).*,
|
||||
\(ViewModel.linkPreviewAttachmentKey).*,
|
||||
|
||||
|
||||
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey),
|
||||
|
||||
-- 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)
|
||||
JOIN \(SessionThread.self) ON \(thread[.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 \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId])
|
||||
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
|
||||
LEFT JOIN (
|
||||
SELECT \(quote[.interactionId]),
|
||||
\(quote[.authorId]),
|
||||
\(quote[.timestampMs]),
|
||||
\(interaction[.body]) AS \(Quote.Columns.body),
|
||||
\(interactionAttachment[.attachmentId]) AS \(Quote.Columns.attachmentId)
|
||||
FROM \(Quote.self)
|
||||
LEFT JOIN \(Interaction.self) ON (
|
||||
(
|
||||
\(quote[.authorId]) = \(interaction[.authorId]) OR (
|
||||
\(quote[.authorId]) = \(blindedPublicKey ?? "") AND
|
||||
\(userPublicKey) = \(interaction[.authorId])
|
||||
)
|
||||
) AND
|
||||
\(quote[.timestampMs]) = \(interaction[.timestampMs])
|
||||
LEFT JOIN \(Quote.self) ON \(quote[.interactionId]) = \(interaction[.id])
|
||||
LEFT JOIN \(Interaction.self) AS \(quoteInteraction) ON (
|
||||
\(quoteInteraction).\(timestampMsColumn) = \(quote[.timestampMs]) AND (
|
||||
\(quoteInteraction).\(authorIdColumn) = \(quote[.authorId]) OR (
|
||||
-- A users outgoing message is stored in some cases using their standard id
|
||||
-- but the quote will use their blinded id so handle that case
|
||||
\(quote[.authorId]) = \(blindedPublicKey ?? "''") AND
|
||||
\(quoteInteraction).\(authorIdColumn) = \(userPublicKey)
|
||||
)
|
||||
)
|
||||
LEFT JOIN \(InteractionAttachment.self) ON \(interaction[.id]) = \(interactionAttachment[.interactionId])
|
||||
) AS \(ViewModel.quoteKey) ON \(quote[.interactionId]) = \(interaction[.id])
|
||||
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumnLiteral) = \(quote[.attachmentId])
|
||||
)
|
||||
LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON (
|
||||
\(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 (
|
||||
\(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 (
|
||||
-- Ignore 'skipped' states
|
||||
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
|
||||
\(recipientState[.interactionId]) = \(interaction[.id])
|
||||
)
|
||||
LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON (
|
||||
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND
|
||||
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral)
|
||||
)
|
||||
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)"))
|
||||
LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON (
|
||||
\(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND
|
||||
\(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id])
|
||||
)
|
||||
WHERE \(interaction.alias[Column.rowID]) IN \(rowIds)
|
||||
\(finalGroupSQL)
|
||||
|
|
|
@ -448,7 +448,8 @@ public extension SessionThreadViewModel {
|
|||
let interactionAttachment: TypedTableAlias<InteractionAttachment> = 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 readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt")
|
||||
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 interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.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
|
||||
/// 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
|
||||
let numColumnsBeforeProfiles: Int = 12
|
||||
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined
|
||||
|
||||
let request: SQLRequest<ViewModel> = """
|
||||
SELECT
|
||||
\(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey),
|
||||
\(thread[.id]) AS \(ViewModel.threadIdKey),
|
||||
\(thread[.variant]) AS \(ViewModel.threadVariantKey),
|
||||
\(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey),
|
||||
|
||||
|
||||
(\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey),
|
||||
\(thread[.isPinned]) AS \(ViewModel.threadIsPinnedKey),
|
||||
\(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey),
|
||||
\(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey),
|
||||
\(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey),
|
||||
|
||||
|
||||
(\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey),
|
||||
\(Interaction.self).\(ViewModel.threadUnreadCountKey),
|
||||
\(Interaction.self).\(ViewModel.threadUnreadMentionCountKey),
|
||||
|
||||
\(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey),
|
||||
\(aggregateInteractionLiteral).\(ViewModel.threadUnreadMentionCountKey),
|
||||
|
||||
\(ViewModel.contactProfileKey).*,
|
||||
\(ViewModel.closedGroupProfileFrontKey).*,
|
||||
\(ViewModel.closedGroupProfileBackKey).*,
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).*,
|
||||
\(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[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey),
|
||||
|
||||
\(Interaction.self).\(ViewModel.interactionIdKey),
|
||||
\(Interaction.self).\(ViewModel.interactionVariantKey),
|
||||
\(Interaction.self).\(interactionTimestampMsColumnLiteral) AS \(ViewModel.interactionTimestampMsKey),
|
||||
\(Interaction.self).\(ViewModel.interactionBodyKey),
|
||||
|
||||
|
||||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
|
||||
\(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey),
|
||||
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
|
||||
|
||||
-- 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),
|
||||
(\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey),
|
||||
|
||||
|
||||
-- These 4 properties will be combined into 'Attachment.DescriptionInfo'
|
||||
\(attachment[.id]),
|
||||
\(attachment[.variant]),
|
||||
\(attachment[.contentType]),
|
||||
\(attachment[.sourceFilename]),
|
||||
COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey),
|
||||
|
||||
|
||||
\(interaction[.authorId]),
|
||||
IFNULL(\(ViewModel.contactProfileKey).\(profileNicknameColumnLiteral), \(ViewModel.contactProfileKey).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey),
|
||||
IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey),
|
||||
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
|
||||
|
||||
|
||||
FROM \(SessionThread.self)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id])
|
||||
|
||||
LEFT JOIN (
|
||||
-- Fetch all interaction-specific data in a subquery to be more efficient
|
||||
SELECT
|
||||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||
\(interaction[.threadId]),
|
||||
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
|
||||
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral),
|
||||
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
|
||||
\(interaction[.authorId]),
|
||||
\(interaction[.linkPreviewUrl]),
|
||||
|
||||
\(interaction[.threadId]) AS \(ViewModel.threadIdKey),
|
||||
MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral),
|
||||
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey),
|
||||
SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey)
|
||||
|
||||
FROM \(Interaction.self)
|
||||
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
|
||||
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 (
|
||||
-- Ignore 'skipped' states
|
||||
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
|
||||
\(recipientState[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey)
|
||||
LEFT JOIN \(Interaction.self) ON (
|
||||
\(interaction[.threadId]) = \(thread[.id]) AND
|
||||
\(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey)
|
||||
)
|
||||
|
||||
LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON (
|
||||
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND
|
||||
\(Interaction.self).\(ViewModel.interactionIdKey) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral)
|
||||
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) AND
|
||||
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL
|
||||
)
|
||||
LEFT JOIN \(LinkPreview.self) ON (
|
||||
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
|
||||
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND
|
||||
\(Interaction.linkPreviewFilterLiteral(timestampColumn: interactionTimestampMsColumnLiteral))
|
||||
\(Interaction.linkPreviewFilterLiteral) AND
|
||||
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)"))
|
||||
)
|
||||
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(Interaction.self).\(ViewModel.interactionIdKey)
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id]) AND
|
||||
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0
|
||||
)
|
||||
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])
|
||||
|
||||
|
||||
-- Thread naming & avatar content
|
||||
|
||||
|
||||
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
|
||||
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.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 (
|
||||
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
|
||||
SELECT MIN(\(groupMember[.profileId]))
|
||||
FROM \(GroupMember.self)
|
||||
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(SQL("\(groupMember[.profileId]) != \(userPublicKey)"))
|
||||
)
|
||||
)
|
||||
|
@ -599,8 +610,8 @@ public extension SessionThreadViewModel {
|
|||
FROM \(GroupMember.self)
|
||||
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
|
||||
WHERE (
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
|
||||
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
|
||||
\(SQL("\(groupMember[.profileId]) != \(userPublicKey)"))
|
||||
)
|
||||
)
|
||||
|
@ -610,7 +621,7 @@ public extension SessionThreadViewModel {
|
|||
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
|
||||
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)"))
|
||||
)
|
||||
|
||||
|
||||
WHERE \(thread.alias[Column.rowID]) IN \(rowIds)
|
||||
\(groupSQL)
|
||||
ORDER BY \(orderSQL)
|
||||
|
@ -643,14 +654,14 @@ public extension SessionThreadViewModel {
|
|||
let contact: TypedTableAlias<Contact> = 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 """
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
\(interaction[.threadId]),
|
||||
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral)
|
||||
MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral)
|
||||
FROM \(Interaction.self)
|
||||
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
|
||||
GROUP BY \(interaction[.threadId])
|
||||
|
@ -701,7 +712,10 @@ public extension SessionThreadViewModel {
|
|||
let thread: TypedTableAlias<SessionThread> = 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 = {
|
||||
|
@ -725,6 +739,8 @@ public extension SessionThreadViewModel {
|
|||
let openGroup: TypedTableAlias<OpenGroup> = 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 groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
|
||||
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
|
||||
|
@ -760,12 +776,22 @@ public extension SessionThreadViewModel {
|
|||
\(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey),
|
||||
\(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey),
|
||||
|
||||
\(Interaction.self).\(ViewModel.threadUnreadCountKey),
|
||||
\(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey),
|
||||
|
||||
\(ViewModel.contactProfileKey).*,
|
||||
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
|
||||
\(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[.server]) AS \(ViewModel.openGroupServerKey),
|
||||
\(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey),
|
||||
|
@ -773,33 +799,28 @@ public extension SessionThreadViewModel {
|
|||
\(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey),
|
||||
\(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey),
|
||||
|
||||
\(Interaction.self).\(ViewModel.interactionIdKey),
|
||||
\(aggregateInteractionLiteral).\(ViewModel.interactionIdKey),
|
||||
|
||||
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
|
||||
|
||||
FROM \(SessionThread.self)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
-- Fetch all interaction-specific data in a subquery to be more efficient
|
||||
SELECT
|
||||
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
|
||||
\(interaction[.threadId]),
|
||||
MAX(\(interaction[.timestampMs])),
|
||||
|
||||
\(interaction[.threadId]) AS \(ViewModel.threadIdKey),
|
||||
MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral),
|
||||
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey)
|
||||
|
||||
FROM \(Interaction.self)
|
||||
WHERE \(SQL("\(interaction[.threadId]) = \(threadId)"))
|
||||
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
||||
|
||||
WHERE (
|
||||
\(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 \(OpenGroup.self) ON \(openGroup[.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 (
|
||||
SELECT
|
||||
\(groupMember[.groupId]),
|
||||
|
@ -1583,7 +1604,7 @@ public extension SessionThreadViewModel {
|
|||
FROM \(SessionThread.self)
|
||||
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
|
||||
LEFT JOIN (
|
||||
SELECT *, MAX(\(interaction[.timestampMs]))
|
||||
SELECT \(interaction[.threadId]), MAX(\(interaction[.timestampMs]))
|
||||
FROM \(Interaction.self)
|
||||
GROUP BY \(interaction[.threadId])
|
||||
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
|
||||
|
|
|
@ -308,9 +308,16 @@ public enum Preferences {
|
|||
}
|
||||
|
||||
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 !regionCode.contains("CN") && !regionCode.contains("CHN") else { return false }
|
||||
|
||||
return true
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -312,9 +312,9 @@ public final class SnodeAPI {
|
|||
public static func getSnodePool() -> Promise<Set<Snode>> {
|
||||
loadSnodePoolIfNeeded()
|
||||
let now = Date()
|
||||
let hasSnodePoolExpired = given(Storage.shared[.lastSnodePoolRefreshDate]) {
|
||||
now.timeIntervalSince($0) > 2 * 60 * 60
|
||||
}.defaulting(to: true)
|
||||
let hasSnodePoolExpired: Bool = Storage.shared[.lastSnodePoolRefreshDate]
|
||||
.map { now.timeIntervalSince($0) > 2 * 60 * 60 }
|
||||
.defaulting(to: true)
|
||||
let snodePool: Set<Snode> = SnodeAPI.snodePool.wrappedValue
|
||||
|
||||
guard hasInsufficientSnodes || hasSnodePoolExpired else {
|
||||
|
|
|
@ -17,13 +17,13 @@ public extension UIView {
|
|||
|
||||
class func spacer(withWidth width: CGFloat) -> UIView {
|
||||
let view = UIView()
|
||||
view.autoSetDimension(.width, toSize: width)
|
||||
view.set(.width, to: width)
|
||||
return view
|
||||
}
|
||||
|
||||
class func spacer(withHeight height: CGFloat) -> UIView {
|
||||
let view = UIView()
|
||||
view.autoSetDimension(.height, toSize: height)
|
||||
view.set(.height, to: height)
|
||||
return view
|
||||
}
|
||||
|
||||
|
|
|
@ -35,14 +35,3 @@ public func getUserHexEncodedPublicKey(_ db: Database? = nil, dependencies: Depe
|
|||
|
||||
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.
|
||||
//
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#import "UIView+OWS.h"
|
||||
#import "OWSMath.h"
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -591,12 +591,17 @@ private final class JobQueue {
|
|||
}
|
||||
|
||||
fileprivate func appDidBecomeActive(with jobs: [Job], canStart: Bool) {
|
||||
let currentlyRunningJobIds: Set<Int64> = jobsCurrentlyRunning.wrappedValue
|
||||
|
||||
queue.mutate { queue in
|
||||
// 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'
|
||||
// jobs and then brings it back to the foreground)
|
||||
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)
|
||||
}
|
||||
|
@ -784,14 +789,20 @@ private final class JobQueue {
|
|||
guard dependencyInfo.jobs.isEmpty else {
|
||||
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
|
||||
/// 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 = queue
|
||||
.filter { !dependencyInfo.jobs.contains($0) }
|
||||
.inserting(contentsOf: Array(dependencyInfo.jobs), at: 0)
|
||||
.filter { !dependencyJobsNotCurrentlyRunning.contains($0) }
|
||||
.inserting(contentsOf: dependencyJobsNotCurrentlyRunning, at: 0)
|
||||
}
|
||||
handleJobDeferred(nextJob)
|
||||
return
|
||||
|
@ -960,17 +971,22 @@ private final class JobQueue {
|
|||
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
|
||||
/// most likely case is that we want an entire job chain to be completed at the same time rather than being blocked by other
|
||||
/// unrelated jobs)
|
||||
/// Now that the job has been completed we want to insert any jobs that were dependant on it, that aren't already running
|
||||
/// 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
|
||||
/// 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
|
||||
/// removed from the queue, replaced by their dependencies
|
||||
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 = queue
|
||||
.filter { !dependantJobs.contains($0) }
|
||||
.inserting(contentsOf: dependantJobs, at: 0)
|
||||
.filter { !dependantJobsNotCurrentlyRunning.contains($0) }
|
||||
.inserting(contentsOf: dependantJobsNotCurrentlyRunning, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import PureLayout
|
||||
|
||||
// Coincides with Android's max text message length
|
||||
let kMaxMessageBodyCharacterCount = 2000
|
||||
|
|
Loading…
Reference in New Issue