Merge branch 'database-refactor' into emoji-reacts

This commit is contained in:
ryanzhao 2022-08-05 09:54:13 +10:00
commit 99e4614bf8
10 changed files with 136 additions and 110 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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