Merge branch 'database-refactor' into emoji-reacts
This commit is contained in:
commit
99e4614bf8
|
@ -135,25 +135,8 @@ final class QuoteView: UIView {
|
||||||
let fallbackImageName: String = (isAudio ? "attachment_audio" : "actionsheet_document_black")
|
let fallbackImageName: String = (isAudio ? "attachment_audio" : "actionsheet_document_black")
|
||||||
let imageView: UIImageView = UIImageView(
|
let imageView: UIImageView = UIImageView(
|
||||||
image: UIImage(named: fallbackImageName)?
|
image: UIImage(named: fallbackImageName)?
|
||||||
|
.resizedImage(to: CGSize(width: iconSize, height: iconSize))?
|
||||||
.withRenderingMode(.alwaysTemplate)
|
.withRenderingMode(.alwaysTemplate)
|
||||||
.resizedImage(to: CGSize(width: iconSize, height: iconSize))
|
|
||||||
)
|
|
||||||
|
|
||||||
attachment.thumbnail(
|
|
||||||
size: .small,
|
|
||||||
success: { image, _ in
|
|
||||||
guard Thread.isMainThread else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
imageView.image = image
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
imageView.image = image
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
},
|
|
||||||
failure: {}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
imageView.tintColor = .white
|
imageView.tintColor = .white
|
||||||
|
@ -171,6 +154,26 @@ final class QuoteView: UIView {
|
||||||
(isAudio ? "Audio" : "Document")
|
(isAudio ? "Audio" : "Document")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate the thumbnail if needed
|
||||||
|
if attachment.isVisualMedia {
|
||||||
|
attachment.thumbnail(
|
||||||
|
size: .small,
|
||||||
|
success: { image, _ in
|
||||||
|
guard Thread.isMainThread else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
imageView.image = image
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.image = image
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
},
|
||||||
|
failure: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
mainStackView.addArrangedSubview(lineView)
|
mainStackView.addArrangedSubview(lineView)
|
||||||
|
|
|
@ -717,6 +717,8 @@ extension Attachment {
|
||||||
// MARK: - Convenience
|
// MARK: - Convenience
|
||||||
|
|
||||||
extension Attachment {
|
extension Attachment {
|
||||||
|
public static let nonMediaQuoteFileId: String = "NON_MEDIA_QUOTE_FILE_ID"
|
||||||
|
|
||||||
public enum ThumbnailSize {
|
public enum ThumbnailSize {
|
||||||
case small
|
case small
|
||||||
case medium
|
case medium
|
||||||
|
@ -869,7 +871,7 @@ extension Attachment {
|
||||||
return existingImage
|
return existingImage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func cloneAsThumbnail() -> Attachment? {
|
public func cloneAsQuoteThumbnail() -> Attachment? {
|
||||||
let cloneId: String = UUID().uuidString
|
let cloneId: String = UUID().uuidString
|
||||||
let thumbnailName: String = "quoted-thumbnail-\(sourceFilename ?? "null")"
|
let thumbnailName: String = "quoted-thumbnail-\(sourceFilename ?? "null")"
|
||||||
|
|
||||||
|
@ -881,7 +883,22 @@ extension Attachment {
|
||||||
mimeType: OWSMimeTypeImageJpeg,
|
mimeType: OWSMimeTypeImageJpeg,
|
||||||
sourceFilename: thumbnailName
|
sourceFilename: thumbnailName
|
||||||
)
|
)
|
||||||
else { return nil }
|
else {
|
||||||
|
// Non-media files cannot have thumbnails but may be sent as quotes, in these cases we want
|
||||||
|
// to create an attachment in an 'uploaded' state with a hard-coded file id so the messageSend
|
||||||
|
// job doesn't try to upload the attachment (we include the original `serverId` as it's
|
||||||
|
// required for generating the protobuf)
|
||||||
|
return Attachment(
|
||||||
|
id: cloneId,
|
||||||
|
serverId: self.serverId,
|
||||||
|
variant: self.variant,
|
||||||
|
state: .uploaded,
|
||||||
|
contentType: self.contentType,
|
||||||
|
byteCount: 0,
|
||||||
|
downloadUrl: Attachment.nonMediaQuoteFileId,
|
||||||
|
isValid: self.isValid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Try generate the thumbnail
|
// Try generate the thumbnail
|
||||||
var thumbnailData: Data?
|
var thumbnailData: Data?
|
||||||
|
@ -922,6 +939,7 @@ extension Attachment {
|
||||||
return Attachment(
|
return Attachment(
|
||||||
id: cloneId,
|
id: cloneId,
|
||||||
variant: .standard,
|
variant: .standard,
|
||||||
|
state: .downloaded,
|
||||||
contentType: OWSMimeTypeImageJpeg,
|
contentType: OWSMimeTypeImageJpeg,
|
||||||
byteCount: UInt(thumbnailData.count),
|
byteCount: UInt(thumbnailData.count),
|
||||||
sourceFilename: thumbnailName,
|
sourceFilename: thumbnailName,
|
||||||
|
|
|
@ -79,8 +79,8 @@ public extension DisappearingMessagesConfiguration {
|
||||||
|
|
||||||
return String(
|
return String(
|
||||||
format: "OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION".localized(),
|
format: "OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION".localized(),
|
||||||
NSString.formatDurationSeconds(UInt32(floor(durationSeconds)), useShortFormat: false),
|
senderName,
|
||||||
senderName
|
NSString.formatDurationSeconds(UInt32(floor(durationSeconds)), useShortFormat: false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,14 +192,14 @@ public class SMKDisappearingMessagesConfiguration: NSObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: DisappearingMessagesConfiguration = (try DisappearingMessagesConfiguration
|
let config: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration
|
||||||
.fetchOne(db, id: threadId)?
|
.fetchOne(db, id: threadId)
|
||||||
|
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId))
|
||||||
.with(
|
.with(
|
||||||
isEnabled: isEnabled,
|
isEnabled: isEnabled,
|
||||||
durationSeconds: durationSeconds
|
durationSeconds: durationSeconds
|
||||||
)
|
)
|
||||||
.saved(db))
|
.saved(db)
|
||||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId))
|
|
||||||
|
|
||||||
let interaction: Interaction = try Interaction(
|
let interaction: Interaction = try Interaction(
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
|
@ -214,7 +214,7 @@ public class SMKDisappearingMessagesConfiguration: NSObject {
|
||||||
db,
|
db,
|
||||||
message: ExpirationTimerUpdate(
|
message: ExpirationTimerUpdate(
|
||||||
syncTarget: nil,
|
syncTarget: nil,
|
||||||
duration: UInt32(floor(durationSeconds))
|
duration: UInt32(floor(isEnabled ? durationSeconds : 0))
|
||||||
),
|
),
|
||||||
interactionId: interaction.id,
|
interactionId: interaction.id,
|
||||||
in: thread
|
in: thread
|
||||||
|
|
|
@ -113,7 +113,7 @@ public extension Quote {
|
||||||
.map { quotedInteraction -> Attachment? in
|
.map { quotedInteraction -> Attachment? in
|
||||||
// If the quotedInteraction has an attachment then try clone it
|
// If the quotedInteraction has an attachment then try clone it
|
||||||
if let attachment: Attachment = try? quotedInteraction.attachments.fetchOne(db) {
|
if let attachment: Attachment = try? quotedInteraction.attachments.fetchOne(db) {
|
||||||
return attachment.cloneAsThumbnail()
|
return attachment.cloneAsQuoteThumbnail()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise if the quotedInteraction has a link preview, try clone that
|
// Otherwise if the quotedInteraction has a link preview, try clone that
|
||||||
|
@ -121,7 +121,7 @@ public extension Quote {
|
||||||
.fetchOne(db)?
|
.fetchOne(db)?
|
||||||
.attachment
|
.attachment
|
||||||
.fetchOne(db)?
|
.fetchOne(db)?
|
||||||
.cloneAsThumbnail()
|
.cloneAsQuoteThumbnail()
|
||||||
}
|
}
|
||||||
.defaulting(to: Attachment(proto: attachment))
|
.defaulting(to: Attachment(proto: attachment))
|
||||||
.inserted(db)
|
.inserted(db)
|
||||||
|
|
|
@ -43,9 +43,15 @@ public enum AttachmentUploadJob: JobExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: In the AttachmentUploadJob we intentionally don't provide our own db instance to prevent reentrancy
|
// If the attachment is still pending download the hold off on running this job
|
||||||
// issues when the success/failure closures get called before the upload as the JobRunner will attempt to
|
guard attachment.state != .pendingDownload && attachment.state != .downloading else {
|
||||||
// update the state of the job immediately
|
deferred(job)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: In the AttachmentUploadJob we intentionally don't provide our own db instance to prevent
|
||||||
|
// reentrancy issues when the success/failure closures get called before the upload as the JobRunner
|
||||||
|
// will attempt to update the state of the job immediately
|
||||||
attachment.upload(
|
attachment.upload(
|
||||||
queue: queue,
|
queue: queue,
|
||||||
using: { db, data in
|
using: { db, data in
|
||||||
|
|
|
@ -75,7 +75,17 @@ public enum MessageSendJob: JobExecutor {
|
||||||
// but not on the message recipients device - both LinkPreview and Quote can
|
// but not on the message recipients device - both LinkPreview and Quote can
|
||||||
// have this case)
|
// have this case)
|
||||||
try allAttachmentStateInfo
|
try allAttachmentStateInfo
|
||||||
.filter { $0.state == .uploading || $0.state == .failedUpload || $0.state == .downloaded }
|
.filter { attachment -> Bool in
|
||||||
|
// Non-media quotes won't have thumbnails so so don't try to upload them
|
||||||
|
guard attachment.downloadUrl != Attachment.nonMediaQuoteFileId else { return false }
|
||||||
|
|
||||||
|
switch attachment.state {
|
||||||
|
case .uploading, .pendingDownload, .downloading, .failedUpload, .downloaded:
|
||||||
|
return true
|
||||||
|
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
.filter { stateInfo in
|
.filter { stateInfo in
|
||||||
// Don't add a new job if there is one already in the queue
|
// Don't add a new job if there is one already in the queue
|
||||||
!JobRunner.hasPendingOrRunningJob(
|
!JobRunner.hasPendingOrRunningJob(
|
||||||
|
|
|
@ -184,31 +184,21 @@ public final class ClosedGroupPoller {
|
||||||
guard isBackgroundPoll || poller?.isPolling.wrappedValue[groupPublicKey] == true else { return Promise.value(()) }
|
guard isBackgroundPoll || poller?.isPolling.wrappedValue[groupPublicKey] == true else { return Promise.value(()) }
|
||||||
|
|
||||||
var promises: [Promise<Void>] = []
|
var promises: [Promise<Void>] = []
|
||||||
var messageCount: Int = 0
|
let allMessages: [SnodeReceivedMessage] = messageResults
|
||||||
let totalMessagesCount: Int = messageResults
|
.reduce([]) { result, next in
|
||||||
.map { result -> Int in
|
switch next {
|
||||||
switch result {
|
case .fulfilled(let messages): return result.appending(contentsOf: messages)
|
||||||
case .fulfilled(let messages): return messages.count
|
default: return result
|
||||||
default: return 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.reduce(0, +)
|
var messageCount: Int = 0
|
||||||
|
let totalMessagesCount: Int = allMessages.count
|
||||||
|
|
||||||
messageResults.forEach { result in
|
Storage.shared.write { db in
|
||||||
guard case .fulfilled(let messages) = result else { return }
|
let processedMessages: [ProcessedMessage] = allMessages
|
||||||
guard !messages.isEmpty else { return }
|
.compactMap { message -> ProcessedMessage? in
|
||||||
|
|
||||||
var jobToRun: Job?
|
|
||||||
|
|
||||||
Storage.shared.write { db in
|
|
||||||
var jobDetailMessages: [MessageReceiveJob.Details.MessageInfo] = []
|
|
||||||
|
|
||||||
messages.forEach { message in
|
|
||||||
do {
|
do {
|
||||||
let processedMessage: ProcessedMessage? = try Message.processRawReceivedMessage(db, rawMessage: message)
|
return try Message.processRawReceivedMessage(db, rawMessage: message)
|
||||||
|
|
||||||
jobDetailMessages = jobDetailMessages
|
|
||||||
.appending(processedMessage?.messageInfo)
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
switch error {
|
switch error {
|
||||||
|
@ -219,28 +209,30 @@ public final class ClosedGroupPoller {
|
||||||
MessageReceiverError.duplicateControlMessage,
|
MessageReceiverError.duplicateControlMessage,
|
||||||
MessageReceiverError.selfSend:
|
MessageReceiverError.selfSend:
|
||||||
break
|
break
|
||||||
|
|
||||||
default: SNLog("Failed to deserialize envelope due to error: \(error).")
|
default: SNLog("Failed to deserialize envelope due to error: \(error).")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messageCount += jobDetailMessages.count
|
|
||||||
jobToRun = Job(
|
|
||||||
variant: .messageReceive,
|
|
||||||
behaviour: .runOnce,
|
|
||||||
threadId: groupPublicKey,
|
|
||||||
details: MessageReceiveJob.Details(
|
|
||||||
messages: jobDetailMessages,
|
|
||||||
isBackgroundPoll: isBackgroundPoll
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// If we are force-polling then add to the JobRunner so they are persistent and will retry on
|
|
||||||
// the next app run if they fail but don't let them auto-start
|
|
||||||
JobRunner.add(db, job: jobToRun, canStartJob: !isBackgroundPoll)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
messageCount = processedMessages.count
|
||||||
|
|
||||||
|
let jobToRun: Job? = Job(
|
||||||
|
variant: .messageReceive,
|
||||||
|
behaviour: .runOnce,
|
||||||
|
threadId: groupPublicKey,
|
||||||
|
details: MessageReceiveJob.Details(
|
||||||
|
messages: processedMessages.map { $0.messageInfo },
|
||||||
|
isBackgroundPoll: isBackgroundPoll
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we are force-polling then add to the JobRunner so they are persistent and will retry on
|
||||||
|
// the next app run if they fail but don't let them auto-start
|
||||||
|
JobRunner.add(db, job: jobToRun, canStartJob: !isBackgroundPoll)
|
||||||
|
|
||||||
// We want to try to handle the receive jobs immediately in the background
|
// We want to try to handle the receive jobs immediately in the background
|
||||||
if isBackgroundPoll {
|
if isBackgroundPoll {
|
||||||
promises = promises.appending(
|
promises = promises.appending(
|
||||||
|
|
|
@ -136,49 +136,44 @@ public final class Poller {
|
||||||
var messageCount: Int = 0
|
var messageCount: Int = 0
|
||||||
|
|
||||||
Storage.shared.write { db in
|
Storage.shared.write { db in
|
||||||
var threadMessages: [String: [MessageReceiveJob.Details.MessageInfo]] = [:]
|
messages
|
||||||
|
.compactMap { message -> ProcessedMessage? in
|
||||||
messages.forEach { message in
|
do {
|
||||||
do {
|
return try Message.processRawReceivedMessage(db, rawMessage: message)
|
||||||
let processedMessage: ProcessedMessage? = try Message.processRawReceivedMessage(db, rawMessage: message)
|
}
|
||||||
let key: String = (processedMessage?.threadId ?? Message.nonThreadMessageId)
|
catch {
|
||||||
|
switch error {
|
||||||
threadMessages[key] = (threadMessages[key] ?? [])
|
// Ignore duplicate & selfSend message errors (and don't bother logging
|
||||||
.appending(processedMessage?.messageInfo)
|
// them as there will be a lot since we each service node duplicates messages)
|
||||||
}
|
case DatabaseError.SQLITE_CONSTRAINT_UNIQUE,
|
||||||
catch {
|
MessageReceiverError.duplicateMessage,
|
||||||
switch error {
|
MessageReceiverError.duplicateControlMessage,
|
||||||
// Ignore duplicate & selfSend message errors (and don't bother logging
|
MessageReceiverError.selfSend:
|
||||||
// them as there will be a lot since we each service node duplicates messages)
|
break
|
||||||
case DatabaseError.SQLITE_CONSTRAINT_UNIQUE,
|
|
||||||
MessageReceiverError.duplicateMessage,
|
default: SNLog("Failed to deserialize envelope due to error: \(error).")
|
||||||
MessageReceiverError.duplicateControlMessage,
|
}
|
||||||
MessageReceiverError.selfSend:
|
|
||||||
break
|
|
||||||
|
|
||||||
default: SNLog("Failed to deserialize envelope due to error: \(error).")
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.grouped { threadId, _, _ in (threadId ?? Message.nonThreadMessageId) }
|
||||||
|
.forEach { threadId, threadMessages in
|
||||||
messageCount = threadMessages
|
messageCount += threadMessages.count
|
||||||
.values
|
|
||||||
.reduce(into: 0) { prev, next in prev += next.count }
|
JobRunner.add(
|
||||||
|
db,
|
||||||
threadMessages.forEach { threadId, threadMessages in
|
job: Job(
|
||||||
JobRunner.add(
|
variant: .messageReceive,
|
||||||
db,
|
behaviour: .runOnce,
|
||||||
job: Job(
|
threadId: threadId,
|
||||||
variant: .messageReceive,
|
details: MessageReceiveJob.Details(
|
||||||
behaviour: .runOnce,
|
messages: threadMessages.map { $0.messageInfo },
|
||||||
threadId: threadId,
|
isBackgroundPoll: false
|
||||||
details: MessageReceiveJob.Details(
|
)
|
||||||
messages: threadMessages,
|
|
||||||
isBackgroundPoll: false
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SNLog("Received \(messageCount) new message\(messageCount == 1 ? "" : "s") (duplicates: \(messages.count - messageCount))")
|
SNLog("Received \(messageCount) new message\(messageCount == 1 ? "" : "s") (duplicates: \(messages.count - messageCount))")
|
||||||
|
|
|
@ -69,7 +69,7 @@ public extension QuotedReplyModel {
|
||||||
guard let sourceAttachment: Attachment = self.attachment else { return nil }
|
guard let sourceAttachment: Attachment = self.attachment else { return nil }
|
||||||
|
|
||||||
return try sourceAttachment
|
return try sourceAttachment
|
||||||
.cloneAsThumbnail()?
|
.cloneAsQuoteThumbnail()?
|
||||||
.inserted(db)
|
.inserted(db)
|
||||||
.id
|
.id
|
||||||
}
|
}
|
||||||
|
|
|
@ -688,9 +688,11 @@ private final class JobQueue {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scheduleNextSoonestJob() {
|
private func scheduleNextSoonestJob() {
|
||||||
|
let jobIdsAlreadyRunning: Set<Int64> = jobsCurrentlyRunning.wrappedValue
|
||||||
let nextJobTimestamp: TimeInterval? = Storage.shared.read { db in
|
let nextJobTimestamp: TimeInterval? = Storage.shared.read { db in
|
||||||
try Job.filterPendingJobs(variants: jobVariants, excludeFutureJobs: false)
|
try Job.filterPendingJobs(variants: jobVariants, excludeFutureJobs: false)
|
||||||
.select(.nextRunTimestamp)
|
.select(.nextRunTimestamp)
|
||||||
|
.filter(!jobIdsAlreadyRunning.contains(Job.Columns.id)) // Exclude jobs already running
|
||||||
.asRequest(of: TimeInterval.self)
|
.asRequest(of: TimeInterval.self)
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue