Updated the CI and fixed a couple of config bugs

Updated to the 1.0.0 release of libSession
Set the User Config feature flag to July 31st 10am AEST
Shifted quote thumbnail generation out of the DBWrite thread
Stopped the CurrentUserPoller from polling the user config namespaces if the feature flag is off
Fixed an issue where the scrollToBottom behaviour could be a little buggy when an optimistic update is replaced with the proper change
Fixed an issue where the 'attachmentsNotUploaded' error wouldn't result in a message entering an error state
Fixed a bug where sync messages with attachments weren't being sent
This commit is contained in:
Morgan Pretty 2023-07-13 14:47:10 +10:00
parent 2833cef5e4
commit b72bf42605
13 changed files with 309 additions and 284 deletions

View File

@ -7,23 +7,6 @@ local clone_submodules = {
// cmake options for static deps mirror // cmake options for static deps mirror
local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https://oxen.rocks/deps ' else ''); local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https://oxen.rocks/deps ' else '');
// xcpretty
local install_xcpretty = {
name: 'Install XCPretty',
commands: [
|||
if [[ $(command -v brew) != "" ]]; then
brew install xcpretty
fi
|||,
|||
if [[ $(command -v brew) == "" ]]; then
gem install xcpretty
fi
|||,
]
};
// Cocoapods // Cocoapods
// //
// Unfortunately Cocoapods has a dumb restriction which requires you to use UTF-8 for the // Unfortunately Cocoapods has a dumb restriction which requires you to use UTF-8 for the
@ -35,81 +18,89 @@ local install_cocoapods = {
[ [
// Unit tests
{ {
kind: 'pipeline', kind: 'pipeline',
type: 'exec', type: 'exec',
name: 'Test Upload', name: 'Unit Tests',
platform: { os: 'darwin', arch: 'amd64' }, platform: { os: 'darwin', arch: 'amd64' },
steps: [ steps: [
clone_submodules,
// install_xcpretty,
install_cocoapods,
{
name: 'Run Unit Tests',
commands: [
'mkdir build',
|||
if command -v xcpretty >/dev/null 2>&1; then
'xcodebuild test -workspace Session.xcworkspace -scheme Session -destination "platform=iOS Simulator,name=iPhone 14 Pro" | xcpretty'
else
'xcodebuild test -workspace Session.xcworkspace -scheme Session -destination "platform=iOS Simulator,name=iPhone 14 Pro"'
fi
|||
],
},
],
},
// Simulator build
{
kind: 'pipeline',
type: 'exec',
name: 'Simulator Build',
platform: { os: 'darwin', arch: 'amd64' },
steps: [
clone_submodules,
install_cocoapods,
{
name: 'Build',
commands: [
'mkdir build',
|||
if command -v xcpretty >/dev/null 2>&1; then
xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration 'App Store Release' -sdk iphonesimulator -derivedDataPath ./build -archivePath ./build/Session_sim.xcarchive -destination 'generic/platform=iOS Simulator' | xcpretty
else
xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration 'App Store Release' -sdk iphonesimulator -derivedDataPath ./build -archivePath ./build/Session_sim.xcarchive -destination 'generic/platform=iOS Simulator'
fi
|||
],
},
{ {
name: 'Upload artifacts', name: 'Upload artifacts',
commands: [ commands: [
'./.drone-static-upload.sh' './Scripts/drone-static-upload.sh'
] ]
} },
] ],
},
// AppStore build (generate an archive to be signed later)
{
kind: 'pipeline',
type: 'exec',
name: 'AppStore Build',
platform: { os: 'darwin', arch: 'amd64' },
steps: [
clone_submodules,
install_cocoapods,
{
name: 'Build',
commands: [
'mkdir build',
|||
if command -v xcpretty >/dev/null 2>&1; then
xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration 'App Store Release' -sdk iphoneos -derivedDataPath ./build -archivePath ./build/Session.xcarchive -destination 'generic/platform=iOS' | xcpretty
else
xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration 'App Store Release' -sdk iphoneos -derivedDataPath ./build -archivePath ./build/Session.xcarchive -destination 'generic/platform=iOS'
fi
|||
],
},
{
name: 'Upload artifacts',
commands: [
'./Scripts/drone-static-upload.sh'
]
},
],
}, },
// // Unit tests
// {
// kind: 'pipeline',
// type: 'exec',
// name: 'Unit Tests',
// platform: { os: 'darwin', arch: 'amd64' },
// steps: [
// clone_submodules,
// // install_xcpretty,
// install_cocoapods,
// {
// name: 'Run Unit Tests',
// commands: [
// 'mkdir build',
// 'xcodebuild test -workspace Session.xcworkspace -scheme Session -destination "platform=iOS Simulator,name=iPhone 14 Pro"' // | xcpretty --report html'
// ],
// },
// ],
// },
// // Simulator build
// {
// kind: 'pipeline',
// type: 'exec',
// name: 'Simulator Build',
// platform: { os: 'darwin', arch: 'amd64' },
// steps: [
// clone_submodules,
// // install_xcpretty,
// install_cocoapods,
// {
// name: 'Build',
// commands: [
// 'mkdir build',
// 'xcodebuild -workspace Session.xcworkspace -scheme Session -configuration "App Store Release" -sdk iphonesimulator -derivedDataPath ./build -destination "generic/platform=iOS Simulator"' // | xcpretty'
// ],
// },
// {
// name: 'Upload artifacts',
// commands: [
// './.drone-static-upload.sh'
// ]
// }
// ],
// },
// // AppStore build (generate an archive to be signed later)
// {
// kind: 'pipeline',
// type: 'exec',
// name: 'AppStore Build',
// platform: { os: 'darwin', arch: 'amd64' },
// steps: [
// clone_submodules,
// // install_xcpretty,
// install_cocoapods,
// {
// name: 'Build',
// commands: [
// 'mkdir build',
// 'xcodebuild archive -workspace Session.xcworkspace -scheme Session -archivePath ./build/Session.xcarchive -destination "platform=generic/iOS" | xcpretty'
// ],
// },
// ],
// },
] ]

@ -1 +1 @@
Subproject commit e0b994201a016cc5bf9065526a0ceb4291f60d5a Subproject commit d8f07fa92c12c5c2409774e03e03395d7847d1c2

View File

@ -26,6 +26,7 @@ var pathFiles: [String] = {
return fileUrls return fileUrls
.filter { .filter {
((try? $0.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == false) && // No directories ((try? $0.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == false) && // No directories
!$0.path.contains("build/") && // Exclude files under the build folder (CI)
!$0.path.contains("Pods/") && // Exclude files under the pods folder !$0.path.contains("Pods/") && // Exclude files under the pods folder
!$0.path.contains(".xcassets") && // Exclude asset bundles !$0.path.contains(".xcassets") && // Exclude asset bundles
!$0.path.contains(".app/") && // Exclude files in the app build directories !$0.path.contains(".app/") && // Exclude files in the app build directories

View File

@ -31,10 +31,20 @@ fi
mkdir -v "$base" mkdir -v "$base"
# Copy over the build products # Copy over the build products
prod_path="build/Session.xcarchive"
sim_path="build/Session_sim.xcarchive/Products/Applications/Session.app"
mkdir build mkdir build
echo "Test" > "build/test.txt" echo "Test" > "build/test.txt"
cp -av build/test.txt "$base"
# cp -av build/Build/Products/App\ Store\ Release-iphonesimulator/Session.app "$base" if [ ! -d $prod_path ]; then
cp -av $prod_path "$base"
else if [ ! -d $sim_path ]; then
cp -av $sim_path "$base"
else
echo "Expected a file to upload, found none" >&2
exit 1
fi
# tar dat shiz up yo # tar dat shiz up yo
archive="$base.tar.xz" archive="$base.tar.xz"
@ -54,9 +64,7 @@ for p in "${upload_dirs[@]}"; do
mkdirs="$mkdirs mkdirs="$mkdirs
-mkdir $dir_tmp" -mkdir $dir_tmp"
done done
if [ -e "$base-debug-symbols.tar.xz" ] ; then
put_debug="put $base-debug-symbols.tar.xz $upload_to"
fi
sftp -i ssh_key -b - -o StrictHostKeyChecking=off drone@oxen.rocks <<SFTP sftp -i ssh_key -b - -o StrictHostKeyChecking=off drone@oxen.rocks <<SFTP
$mkdirs $mkdirs
put $archive $upload_to put $archive $upload_to

View File

@ -453,6 +453,7 @@ extension ConversationVC:
self?.snInputView.quoteDraftInfo = nil self?.snInputView.quoteDraftInfo = nil
self?.resetMentions() self?.resetMentions()
self?.scrollToBottom(isAnimated: false)
} }
// Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can // Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can
@ -481,64 +482,70 @@ extension ConversationVC:
quoteModel: quoteModel quoteModel: quoteModel
) )
// Actually send the message DispatchQueue.global(qos:.userInitiated).async {
Storage.shared // Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as
.writePublisher { [weak self] db in // this can take up to 0.5s
// Update the thread to be visible (if it isn't already) let quoteThumbnailAttachment: Attachment? = quoteModel?.attachment?.cloneAsQuoteThumbnail()
if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread // Actually send the message
.filter(id: threadId) Storage.shared
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true)) .writePublisher { [weak self] db in
// Update the thread to be visible (if it isn't already)
if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread
.filter(id: threadId)
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
}
// Insert the interaction and associated it with the optimistically inserted message so
// we can remove it once the database triggers a UI update
let insertedInteraction: Interaction = try optimisticData.interaction.inserted(db)
self?.viewModel.associate(optimisticMessageId: optimisticData.id, to: insertedInteraction.id)
// If there is a LinkPreview and it doesn't match an existing one then add it now
if
let linkPreviewDraft: LinkPreviewDraft = linkPreviewDraft,
(try? insertedInteraction.linkPreview.isEmpty(db)) == true
{
try LinkPreview(
url: linkPreviewDraft.urlString,
title: linkPreviewDraft.title,
attachmentId: try optimisticData.linkPreviewAttachment?.inserted(db).id
).insert(db)
}
// If there is a Quote the insert it now
if let interactionId: Int64 = insertedInteraction.id, let quoteModel: QuotedReplyModel = quoteModel {
try Quote(
interactionId: interactionId,
authorId: quoteModel.authorId,
timestampMs: quoteModel.timestampMs,
body: quoteModel.body,
attachmentId: try quoteThumbnailAttachment?.inserted(db).id
).insert(db)
}
// Process any attachments
try Attachment.process(
db,
data: optimisticData.attachmentData,
for: insertedInteraction.id
)
try MessageSender.send(
db,
interaction: insertedInteraction,
threadId: threadId,
threadVariant: threadVariant
)
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
// Insert the interaction and associated it with the optimistically inserted message so .sinkUntilComplete(
// we can remove it once the database triggers a UI update receiveCompletion: { [weak self] _ in
let insertedInteraction: Interaction = try optimisticData.interaction.inserted(db) self?.handleMessageSent()
self?.viewModel.associate(optimisticMessageId: optimisticData.id, to: insertedInteraction.id) }
// If there is a LinkPreview and it doesn't match an existing one then add it now
if
let linkPreviewDraft: LinkPreviewDraft = linkPreviewDraft,
(try? insertedInteraction.linkPreview.isEmpty(db)) == true
{
try LinkPreview(
url: linkPreviewDraft.urlString,
title: linkPreviewDraft.title,
attachmentId: try optimisticData.linkPreviewAttachment?.inserted(db).id
).insert(db)
}
// If there is a Quote the insert it now
if let interactionId: Int64 = insertedInteraction.id, let quoteModel: QuotedReplyModel = quoteModel {
try Quote(
interactionId: interactionId,
authorId: quoteModel.authorId,
timestampMs: quoteModel.timestampMs,
body: quoteModel.body,
attachmentId: quoteModel.generateAttachmentThumbnailIfNeeded(db)
).insert(db)
}
// Process any attachments
try Attachment.process(
db,
data: optimisticData.attachmentData,
for: insertedInteraction.id
) )
}
try MessageSender.send(
db,
interaction: insertedInteraction,
threadId: threadId,
threadVariant: threadVariant
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete(
receiveCompletion: { [weak self] _ in
self?.handleMessageSent()
}
)
} }
func handleMessageSent() { func handleMessageSent() {

View File

@ -899,9 +899,34 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
// Store the 'sentMessageBeforeUpdate' state locally // Store the 'sentMessageBeforeUpdate' state locally
let didSendMessageBeforeUpdate: Bool = self.viewModel.sentMessageBeforeUpdate let didSendMessageBeforeUpdate: Bool = self.viewModel.sentMessageBeforeUpdate
let onlyReplacedOptimisticUpdate: Bool = {
// Replacing an optimistic update means making a delete and an insert, which will be done
// as separate changes at the same positions
guard
changeset.count > 1 &&
changeset[changeset.count - 2].elementDeleted == changeset[changeset.count - 1].elementInserted
else { return false }
let deletedModels: [MessageViewModel] = changeset[changeset.count - 2]
.elementDeleted
.map { self.viewModel.interactionData[$0.section].elements[$0.element] }
let insertedModels: [MessageViewModel] = changeset[changeset.count - 1]
.elementInserted
.map { updatedData[$0.section].elements[$0.element] }
// Make sure all the deleted models were optimistic updates, the inserted models were not
// optimistic updates and they have the same timestamps
return (
deletedModels.map { $0.id }.asSet() == [MessageViewModel.optimisticUpdateId] &&
insertedModels.map { $0.id }.asSet() != [MessageViewModel.optimisticUpdateId] &&
deletedModels.map { $0.timestampMs }.asSet() == insertedModels.map { $0.timestampMs }.asSet()
)
}()
let wasOnlyUpdates: Bool = ( let wasOnlyUpdates: Bool = (
changeset.count == 1 && onlyReplacedOptimisticUpdate || (
changeset[0].elementUpdated.count == changeset[0].changeCount changeset.count == 1 &&
changeset[0].elementUpdated.count == changeset[0].changeCount
)
) )
self.viewModel.sentMessageBeforeUpdate = false self.viewModel.sentMessageBeforeUpdate = false
@ -912,13 +937,13 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
guard !didSendMessageBeforeUpdate && !wasOnlyUpdates else { guard !didSendMessageBeforeUpdate && !wasOnlyUpdates else {
self.viewModel.updateInteractionData(updatedData) self.viewModel.updateInteractionData(updatedData)
self.tableView.reloadData() self.tableView.reloadData()
self.tableView.layoutIfNeeded()
// If we just sent a message then we want to jump to the bottom of the conversation instantly // If we just sent a message then we want to jump to the bottom of the conversation instantly
if didSendMessageBeforeUpdate { if didSendMessageBeforeUpdate {
// We need to dispatch to the next run loop because it seems trying to scroll immediately after // We need to dispatch to the next run loop because it seems trying to scroll immediately after
// triggering a 'reloadData' doesn't work // triggering a 'reloadData' doesn't work
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.tableView.layoutIfNeeded()
self?.scrollToBottom(isAnimated: false) self?.scrollToBottom(isAnimated: false)
// Note: The scroll button alpha won't get set correctly in this case so we forcibly set it to // Note: The scroll button alpha won't get set correctly in this case so we forcibly set it to

View File

@ -299,7 +299,7 @@ enum GiphyAPI {
return HTTPError.generic return HTTPError.generic
} }
.map { data, _ in .map { data, _ in
Logger.error("search request succeeded") Logger.debug("search request succeeded")
guard let imageInfos = self.parseGiphyImages(responseData: data) else { guard let imageInfos = self.parseGiphyImages(responseData: data) else {
Logger.error("unable to parse trending images") Logger.error("unable to parse trending images")
@ -347,7 +347,7 @@ enum GiphyAPI {
return HTTPError.generic return HTTPError.generic
} }
.tryMap { data, _ -> [GiphyImageInfo] in .tryMap { data, _ -> [GiphyImageInfo] in
Logger.error("search request succeeded") Logger.debug("search request succeeded")
guard let imageInfos = self.parseGiphyImages(responseData: data) else { guard let imageInfos = self.parseGiphyImages(responseData: data) else {
throw HTTPError.invalidResponse throw HTTPError.invalidResponse

View File

@ -39,8 +39,7 @@ public enum MessageSendJob: JobExecutor {
/// already have attachments in a valid state /// already have attachments in a valid state
if if
details.message is VisibleMessage, details.message is VisibleMessage,
(details.message as? VisibleMessage)?.reaction == nil && (details.message as? VisibleMessage)?.reaction == nil
details.isSyncMessage == false
{ {
guard guard
let jobId: Int64 = job.id, let jobId: Int64 = job.id,
@ -51,122 +50,111 @@ public enum MessageSendJob: JobExecutor {
return return
} }
// If the original interaction no longer exists then don't bother sending the message (ie. the // Retrieve the current attachment state
// message was deleted before it even got sent) typealias AttachmentState = (error: Error?, pendingUploadAttachmentIds: [String], preparedFileIds: [String])
guard Storage.shared.read({ db in try Interaction.exists(db, id: interactionId) }) == true else {
SNLog("[MessageSendJob] Failing due to missing interaction") let attachmentState: AttachmentState = Storage.shared
failure(job, StorageError.objectNotFound, true) .read { db in
return // If the original interaction no longer exists then don't bother sending the message (ie. the
} // message was deleted before it even got sent)
guard try Interaction.exists(db, id: interactionId) else {
// Check if there are any attachments associated to this message, and if so SNLog("[MessageSendJob] Failing due to missing interaction")
// upload them now return (StorageError.objectNotFound, [], [])
// }
// Note: Normal attachments should be sent in a non-durable way but any
// attachments for LinkPreviews and Quotes will be processed through this mechanism // Get the current state of the attachments
let attachmentState: (shouldFail: Bool, shouldDefer: Bool, fileIds: [String])? = Storage.shared.write { db in let allAttachmentStateInfo: [Attachment.StateInfo] = try Attachment
let allAttachmentStateInfo: [Attachment.StateInfo] = try Attachment .stateInfo(interactionId: interactionId)
.stateInfo(interactionId: interactionId) .fetchAll(db)
.fetchAll(db) let maybeFileIds: [String?] = allAttachmentStateInfo
let maybeFileIds: [String?] = allAttachmentStateInfo .sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex }
.sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex } .map { Attachment.fileId(for: $0.downloadUrl) }
.map { Attachment.fileId(for: $0.downloadUrl) } let fileIds: [String] = maybeFileIds.compactMap { $0 }
let fileIds: [String] = maybeFileIds.compactMap { $0 }
// If there were failed attachments then this job should fail (can't send a
// If there were failed attachments then this job should fail (can't send a // message which has associated attachments if the attachments fail to upload)
// message which has associated attachments if the attachments fail to upload) guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else {
guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else { SNLog("[MessageSendJob] Failing due to failed attachment upload")
return (true, false, fileIds) return (AttachmentError.notUploaded, [], fileIds)
} }
// Create jobs for any pending (or failed) attachment jobs and insert them into the /// Find all attachmentIds for attachments which need to be uploaded
// queue before the current job (this will mean the current job will re-run ///
// after these inserted jobs complete) /// **Note:** If there are any 'downloaded' attachments then they also need to be uploaded (as a
// /// 'downloaded' attachment will be on the current users device but not on the message recipients
// Note: If there are any 'downloaded' attachments then they also need to be /// device - both `LinkPreview` and `Quote` can have this case)
// uploaded (as a 'downloaded' attachment will be on the current users device let pendingUploadAttachmentIds: [String] = allAttachmentStateInfo
// but not on the message recipients device - both LinkPreview and Quote can .filter { attachment -> Bool in
// have this case) // Non-media quotes won't have thumbnails so so don't try to upload them
try allAttachmentStateInfo guard attachment.downloadUrl != Attachment.nonMediaQuoteFileId else { return false }
.filter { attachment -> Bool in
// Non-media quotes won't have thumbnails so so don't try to upload them switch attachment.state {
guard attachment.downloadUrl != Attachment.nonMediaQuoteFileId else { return false } case .uploading, .pendingDownload, .downloading, .failedUpload, .downloaded:
return true
switch attachment.state {
case .uploading, .pendingDownload, .downloading, .failedUpload, .downloaded: default: return false
return true }
default: return false
} }
} .map { $0.attachmentId }
.filter { stateInfo in
// Don't add a new job if there is one already in the queue return (nil, pendingUploadAttachmentIds, fileIds)
!JobRunner.hasPendingOrRunningJob( }
with: .attachmentUpload, .defaulting(to: (MessageSenderError.invalidMessage, [], []))
details: AttachmentUploadJob.Details(
messageSendJobId: jobId, /// If we got an error when trying to retrieve the attachment state then this job is actually invalid so it
attachmentId: stateInfo.attachmentId /// should permanently fail
) guard attachmentState.error == nil else {
) return failure(job, (attachmentState.error ?? MessageSenderError.invalidMessage), true)
}
.compactMap { stateInfo -> (jobId: Int64, job: Job)? in
JobRunner
.insert(
db,
job: Job(
variant: .attachmentUpload,
behaviour: .runOnce,
threadId: job.threadId,
interactionId: interactionId,
details: AttachmentUploadJob.Details(
messageSendJobId: jobId,
attachmentId: stateInfo.attachmentId
)
),
before: job
)
}
.forEach { otherJobId, _ in
// Create the dependency between the jobs
try JobDependencies(
jobId: jobId,
dependantId: otherJobId
)
.insert(db)
}
// If there were pending or uploading attachments then stop here (we want to
// upload them first and then re-run this send job - the 'JobRunner.insert'
// method will take care of this)
let isMissingFileIds: Bool = (maybeFileIds.count != fileIds.count)
let hasPendingUploads: Bool = allAttachmentStateInfo.contains(where: { $0.state != .uploaded })
return (
(isMissingFileIds && !hasPendingUploads),
hasPendingUploads,
fileIds
)
}
// Don't send messages with failed attachment uploads
//
// Note: If we have gotten to this point then any dependant attachment upload
// jobs will have permanently failed so this message send should also do so
guard attachmentState?.shouldFail == false else {
SNLog("[MessageSendJob] Failing due to failed attachment upload")
failure(job, AttachmentError.notUploaded, true)
return
} }
// Defer the job if we found incomplete uploads /// If we have any pending (or failed) attachment uploads then we should create jobs for them and insert them into the
guard attachmentState?.shouldDefer == false else { /// queue before the current job and defer it (this will mean the current job will re-run after these inserted jobs complete)
SNLog("[MessageSendJob] Deferring pending attachment uploads") guard attachmentState.pendingUploadAttachmentIds.isEmpty else {
deferred(job) Storage.shared.write { db in
return try attachmentState.pendingUploadAttachmentIds
.filter { attachmentId in
// Don't add a new job if there is one already in the queue
!JobRunner.hasPendingOrRunningJob(
with: .attachmentUpload,
details: AttachmentUploadJob.Details(
messageSendJobId: jobId,
attachmentId: attachmentId
)
)
}
.compactMap { attachmentId -> (jobId: Int64, job: Job)? in
JobRunner
.insert(
db,
job: Job(
variant: .attachmentUpload,
behaviour: .runOnce,
threadId: job.threadId,
interactionId: interactionId,
details: AttachmentUploadJob.Details(
messageSendJobId: jobId,
attachmentId: attachmentId
)
),
before: job
)
}
.forEach { otherJobId, _ in
// Create the dependency between the jobs
try JobDependencies(
jobId: jobId,
dependantId: otherJobId
)
.insert(db)
}
}
SNLog("[MessageSendJob] Deferring due to pending attachment uploads")
return deferred(job)
} }
// Store the fileIds so they can be sent with the open group message content // Store the fileIds so they can be sent with the open group message content
messageFileIds = (attachmentState?.fileIds ?? []) messageFileIds = attachmentState.preparedFileIds
} }
// Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error // Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error

View File

@ -606,6 +606,21 @@ public final class MessageSender {
) )
guard expectedAttachmentUploadCount == preparedSendData.totalAttachmentsUploaded else { guard expectedAttachmentUploadCount == preparedSendData.totalAttachmentsUploaded else {
// Make sure to actually handle this as a failure (if we don't then the message
// won't go into an error state correctly)
if let message: Message = preparedSendData.message {
dependencies.storage.read { db in
MessageSender.handleFailedMessageSend(
db,
message: message,
with: .attachmentsNotUploaded,
interactionId: preparedSendData.interactionId,
isSyncMessage: (preparedSendData.isSyncMessage == true),
using: dependencies
)
}
}
return Fail(error: MessageSenderError.attachmentsNotUploaded) return Fail(error: MessageSenderError.attachmentsNotUploaded)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -992,7 +1007,6 @@ public final class MessageSender {
isSyncMessage: Bool = false, isSyncMessage: Bool = false,
using dependencies: SMKDependencies = SMKDependencies() using dependencies: SMKDependencies = SMKDependencies()
) -> Error { ) -> Error {
// TODO: Revert the local database change
// If the message was a reaction then we don't want to do anything to the original // If the message was a reaction then we don't want to do anything to the original
// interaciton (which the 'interactionId' is pointing to // interaciton (which the 'interactionId' is pointing to
guard (message as? VisibleMessage)?.reaction == nil else { return error } guard (message as? VisibleMessage)?.reaction == nil else { return error }

View File

@ -14,7 +14,12 @@ public final class CurrentUserPoller: Poller {
// MARK: - Settings // MARK: - Settings
override var namespaces: [SnodeAPI.Namespace] { CurrentUserPoller.namespaces } override var namespaces: [SnodeAPI.Namespace] {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
guard SessionUtil.userConfigsEnabled else { return [.default] }
return CurrentUserPoller.namespaces
}
/// After polling a given snode this many times we always switch to a new one. /// After polling a given snode this many times we always switch to a new one.
/// ///

View File

@ -71,16 +71,3 @@ public struct QuotedReplyModel {
) )
} }
} }
// MARK: - Convenience
public extension QuotedReplyModel {
func generateAttachmentThumbnailIfNeeded(_ db: Database) throws -> String? {
guard let sourceAttachment: Attachment = self.attachment else { return nil }
return try sourceAttachment
.cloneAsQuoteThumbnail()?
.inserted(db)
.id
}
}

View File

@ -10,9 +10,7 @@ import SessionUtilitiesKit
public extension Features { public extension Features {
static func useSharedUtilForUserConfig(_ db: Database? = nil) -> Bool { static func useSharedUtilForUserConfig(_ db: Database? = nil) -> Bool {
return true guard Date().timeIntervalSince1970 < 1690761600 else { return true }
// TODO: Need to set this timestamp to the correct date (currently start of 2030)
// guard Date().timeIntervalSince1970 < 1893456000 else { return true }
guard !SessionUtil.hasCheckedMigrationsCompleted.wrappedValue else { guard !SessionUtil.hasCheckedMigrationsCompleted.wrappedValue else {
return SessionUtil.userConfigsEnabledIgnoringFeatureFlag return SessionUtil.userConfigsEnabledIgnoringFeatureFlag
} }

View File

@ -527,6 +527,7 @@ public extension MessageViewModel {
public extension MessageViewModel { public extension MessageViewModel {
static let genericId: Int64 = -1 static let genericId: Int64 = -1
static let typingIndicatorId: Int64 = -2 static let typingIndicatorId: Int64 = -2
static let optimisticUpdateId: Int64 = -3
/// This init method is only used for system-created cells or empty states /// This init method is only used for system-created cells or empty states
init( init(
@ -634,8 +635,8 @@ public extension MessageViewModel {
// Interaction Info // Interaction Info
self.rowId = -1 self.rowId = MessageViewModel.optimisticUpdateId
self.id = -1 self.id = MessageViewModel.optimisticUpdateId
self.openGroupServerMessageId = nil self.openGroupServerMessageId = nil
self.variant = .standardOutgoing self.variant = .standardOutgoing
self.timestampMs = timestampMs self.timestampMs = timestampMs