Made a few optimisations and fixes

Made a couple of DB query optimisations for the Home and Conversation screens
Removed some compiler-complex global generic functions
Increased the timeout for file uploads
Fixed a few import issues
Fixed an issue preventing calls on the simulator from working (disable CallKit on the simulator)
Fixed an issue where opening a conversation with a draft would result in a typing indicator notification being sent (if enabled)
Fixed a truncation issue on the CallVC
This commit is contained in:
Morgan Pretty 2023-04-12 16:50:35 +10:00
parent fa1d786ece
commit 70ce967df6
21 changed files with 228 additions and 193 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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>

View File

@ -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]

View File

@ -520,8 +520,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)
)
)
@ -566,8 +565,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)
)
)

View File

@ -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> {

View File

@ -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 }

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -637,27 +637,32 @@ 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 numColumnsBeforeLinkedRecords: Int = 20
let finalGroupSQL: SQL = (groupSQL ?? "")
@ -671,7 +676,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 +690,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 +730,35 @@ 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 (
\(quoteInteraction).\(authorIdColumn) = '' 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)
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)

View File

@ -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])

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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) }

View File

@ -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

View File

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

View File

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