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:
parent
2833cef5e4
commit
b72bf42605
159
.drone.jsonnet
159
.drone.jsonnet
|
@ -7,23 +7,6 @@ local clone_submodules = {
|
|||
// cmake options for static deps mirror
|
||||
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
|
||||
//
|
||||
// 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',
|
||||
type: 'exec',
|
||||
name: 'Test Upload',
|
||||
name: 'Unit Tests',
|
||||
platform: { os: 'darwin', arch: 'amd64' },
|
||||
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',
|
||||
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
|
|
@ -26,6 +26,7 @@ var pathFiles: [String] = {
|
|||
return fileUrls
|
||||
.filter {
|
||||
((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(".xcassets") && // Exclude asset bundles
|
||||
!$0.path.contains(".app/") && // Exclude files in the app build directories
|
||||
|
|
|
@ -31,10 +31,20 @@ fi
|
|||
mkdir -v "$base"
|
||||
|
||||
# Copy over the build products
|
||||
prod_path="build/Session.xcarchive"
|
||||
sim_path="build/Session_sim.xcarchive/Products/Applications/Session.app"
|
||||
|
||||
mkdir build
|
||||
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
|
||||
archive="$base.tar.xz"
|
||||
|
@ -54,9 +64,7 @@ for p in "${upload_dirs[@]}"; do
|
|||
mkdirs="$mkdirs
|
||||
-mkdir $dir_tmp"
|
||||
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
|
||||
$mkdirs
|
||||
put $archive $upload_to
|
|
@ -453,6 +453,7 @@ extension ConversationVC:
|
|||
self?.snInputView.quoteDraftInfo = nil
|
||||
|
||||
self?.resetMentions()
|
||||
self?.scrollToBottom(isAnimated: false)
|
||||
}
|
||||
|
||||
// Note: 'shouldBeVisible' is set to true the first time a thread is saved so we can
|
||||
|
@ -481,64 +482,70 @@ extension ConversationVC:
|
|||
quoteModel: quoteModel
|
||||
)
|
||||
|
||||
// Actually send the message
|
||||
Storage.shared
|
||||
.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))
|
||||
DispatchQueue.global(qos:.userInitiated).async {
|
||||
// Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as
|
||||
// this can take up to 0.5s
|
||||
let quoteThumbnailAttachment: Attachment? = quoteModel?.attachment?.cloneAsQuoteThumbnail()
|
||||
|
||||
// Actually send the message
|
||||
Storage.shared
|
||||
.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
|
||||
)
|
||||
}
|
||||
|
||||
// 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: quoteModel.generateAttachmentThumbnailIfNeeded(db)
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
// Process any attachments
|
||||
try Attachment.process(
|
||||
db,
|
||||
data: optimisticData.attachmentData,
|
||||
for: insertedInteraction.id
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.sinkUntilComplete(
|
||||
receiveCompletion: { [weak self] _ in
|
||||
self?.handleMessageSent()
|
||||
}
|
||||
)
|
||||
|
||||
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() {
|
||||
|
|
|
@ -899,9 +899,34 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
|
||||
// Store the 'sentMessageBeforeUpdate' state locally
|
||||
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 = (
|
||||
changeset.count == 1 &&
|
||||
changeset[0].elementUpdated.count == changeset[0].changeCount
|
||||
onlyReplacedOptimisticUpdate || (
|
||||
changeset.count == 1 &&
|
||||
changeset[0].elementUpdated.count == changeset[0].changeCount
|
||||
)
|
||||
)
|
||||
self.viewModel.sentMessageBeforeUpdate = false
|
||||
|
||||
|
@ -912,13 +937,13 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
guard !didSendMessageBeforeUpdate && !wasOnlyUpdates else {
|
||||
self.viewModel.updateInteractionData(updatedData)
|
||||
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 didSendMessageBeforeUpdate {
|
||||
// We need to dispatch to the next run loop because it seems trying to scroll immediately after
|
||||
// triggering a 'reloadData' doesn't work
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.tableView.layoutIfNeeded()
|
||||
self?.scrollToBottom(isAnimated: false)
|
||||
|
||||
// Note: The scroll button alpha won't get set correctly in this case so we forcibly set it to
|
||||
|
|
|
@ -299,7 +299,7 @@ enum GiphyAPI {
|
|||
return HTTPError.generic
|
||||
}
|
||||
.map { data, _ in
|
||||
Logger.error("search request succeeded")
|
||||
Logger.debug("search request succeeded")
|
||||
|
||||
guard let imageInfos = self.parseGiphyImages(responseData: data) else {
|
||||
Logger.error("unable to parse trending images")
|
||||
|
@ -347,7 +347,7 @@ enum GiphyAPI {
|
|||
return HTTPError.generic
|
||||
}
|
||||
.tryMap { data, _ -> [GiphyImageInfo] in
|
||||
Logger.error("search request succeeded")
|
||||
Logger.debug("search request succeeded")
|
||||
|
||||
guard let imageInfos = self.parseGiphyImages(responseData: data) else {
|
||||
throw HTTPError.invalidResponse
|
||||
|
|
|
@ -39,8 +39,7 @@ public enum MessageSendJob: JobExecutor {
|
|||
/// already have attachments in a valid state
|
||||
if
|
||||
details.message is VisibleMessage,
|
||||
(details.message as? VisibleMessage)?.reaction == nil &&
|
||||
details.isSyncMessage == false
|
||||
(details.message as? VisibleMessage)?.reaction == nil
|
||||
{
|
||||
guard
|
||||
let jobId: Int64 = job.id,
|
||||
|
@ -51,122 +50,111 @@ public enum MessageSendJob: JobExecutor {
|
|||
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 Storage.shared.read({ db in try Interaction.exists(db, id: interactionId) }) == true else {
|
||||
SNLog("[MessageSendJob] Failing due to missing interaction")
|
||||
failure(job, StorageError.objectNotFound, true)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there are any attachments associated to this message, and if so
|
||||
// upload them now
|
||||
//
|
||||
// Note: Normal attachments should be sent in a non-durable way but any
|
||||
// attachments for LinkPreviews and Quotes will be processed through this mechanism
|
||||
let attachmentState: (shouldFail: Bool, shouldDefer: Bool, fileIds: [String])? = Storage.shared.write { db in
|
||||
let allAttachmentStateInfo: [Attachment.StateInfo] = try Attachment
|
||||
.stateInfo(interactionId: interactionId)
|
||||
.fetchAll(db)
|
||||
let maybeFileIds: [String?] = allAttachmentStateInfo
|
||||
.sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex }
|
||||
.map { Attachment.fileId(for: $0.downloadUrl) }
|
||||
let fileIds: [String] = maybeFileIds.compactMap { $0 }
|
||||
|
||||
// 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)
|
||||
guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else {
|
||||
return (true, false, fileIds)
|
||||
}
|
||||
|
||||
// Create jobs for any pending (or failed) attachment jobs and insert them into the
|
||||
// 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 device - both LinkPreview and Quote can
|
||||
// have this case)
|
||||
try allAttachmentStateInfo
|
||||
.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
|
||||
// Retrieve the current attachment state
|
||||
typealias AttachmentState = (error: Error?, pendingUploadAttachmentIds: [String], preparedFileIds: [String])
|
||||
|
||||
let attachmentState: AttachmentState = Storage.shared
|
||||
.read { db in
|
||||
// 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 {
|
||||
SNLog("[MessageSendJob] Failing due to missing interaction")
|
||||
return (StorageError.objectNotFound, [], [])
|
||||
}
|
||||
|
||||
// Get the current state of the attachments
|
||||
let allAttachmentStateInfo: [Attachment.StateInfo] = try Attachment
|
||||
.stateInfo(interactionId: interactionId)
|
||||
.fetchAll(db)
|
||||
let maybeFileIds: [String?] = allAttachmentStateInfo
|
||||
.sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex }
|
||||
.map { Attachment.fileId(for: $0.downloadUrl) }
|
||||
let fileIds: [String] = maybeFileIds.compactMap { $0 }
|
||||
|
||||
// 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)
|
||||
guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else {
|
||||
SNLog("[MessageSendJob] Failing due to failed attachment upload")
|
||||
return (AttachmentError.notUploaded, [], fileIds)
|
||||
}
|
||||
|
||||
/// Find all attachmentIds for attachments which need to be uploaded
|
||||
///
|
||||
/// **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
|
||||
/// device - both `LinkPreview` and `Quote` can have this case)
|
||||
let pendingUploadAttachmentIds: [String] = allAttachmentStateInfo
|
||||
.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
|
||||
// Don't add a new job if there is one already in the queue
|
||||
!JobRunner.hasPendingOrRunningJob(
|
||||
with: .attachmentUpload,
|
||||
details: AttachmentUploadJob.Details(
|
||||
messageSendJobId: jobId,
|
||||
attachmentId: stateInfo.attachmentId
|
||||
)
|
||||
)
|
||||
}
|
||||
.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
|
||||
.map { $0.attachmentId }
|
||||
|
||||
return (nil, pendingUploadAttachmentIds, fileIds)
|
||||
}
|
||||
.defaulting(to: (MessageSenderError.invalidMessage, [], []))
|
||||
|
||||
/// If we got an error when trying to retrieve the attachment state then this job is actually invalid so it
|
||||
/// should permanently fail
|
||||
guard attachmentState.error == nil else {
|
||||
return failure(job, (attachmentState.error ?? MessageSenderError.invalidMessage), true)
|
||||
}
|
||||
|
||||
// Defer the job if we found incomplete uploads
|
||||
guard attachmentState?.shouldDefer == false else {
|
||||
SNLog("[MessageSendJob] Deferring pending attachment uploads")
|
||||
deferred(job)
|
||||
return
|
||||
/// If we have any pending (or failed) attachment uploads then we should create jobs for them and insert them into the
|
||||
/// queue before the current job and defer it (this will mean the current job will re-run after these inserted jobs complete)
|
||||
guard attachmentState.pendingUploadAttachmentIds.isEmpty else {
|
||||
Storage.shared.write { db in
|
||||
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
|
||||
messageFileIds = (attachmentState?.fileIds ?? [])
|
||||
messageFileIds = attachmentState.preparedFileIds
|
||||
}
|
||||
|
||||
// Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error
|
||||
|
|
|
@ -606,6 +606,21 @@ public final class MessageSender {
|
|||
)
|
||||
|
||||
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)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
@ -992,7 +1007,6 @@ public final class MessageSender {
|
|||
isSyncMessage: Bool = false,
|
||||
using dependencies: SMKDependencies = SMKDependencies()
|
||||
) -> Error {
|
||||
// TODO: Revert the local database change
|
||||
// If the message was a reaction then we don't want to do anything to the original
|
||||
// interaciton (which the 'interactionId' is pointing to
|
||||
guard (message as? VisibleMessage)?.reaction == nil else { return error }
|
||||
|
|
|
@ -14,7 +14,12 @@ public final class CurrentUserPoller: Poller {
|
|||
|
||||
// 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.
|
||||
///
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,7 @@ import SessionUtilitiesKit
|
|||
|
||||
public extension Features {
|
||||
static func useSharedUtilForUserConfig(_ db: Database? = nil) -> Bool {
|
||||
return true
|
||||
// TODO: Need to set this timestamp to the correct date (currently start of 2030)
|
||||
// guard Date().timeIntervalSince1970 < 1893456000 else { return true }
|
||||
guard Date().timeIntervalSince1970 < 1690761600 else { return true }
|
||||
guard !SessionUtil.hasCheckedMigrationsCompleted.wrappedValue else {
|
||||
return SessionUtil.userConfigsEnabledIgnoringFeatureFlag
|
||||
}
|
||||
|
|
|
@ -527,6 +527,7 @@ public extension MessageViewModel {
|
|||
public extension MessageViewModel {
|
||||
static let genericId: Int64 = -1
|
||||
static let typingIndicatorId: Int64 = -2
|
||||
static let optimisticUpdateId: Int64 = -3
|
||||
|
||||
/// This init method is only used for system-created cells or empty states
|
||||
init(
|
||||
|
@ -634,8 +635,8 @@ public extension MessageViewModel {
|
|||
|
||||
// Interaction Info
|
||||
|
||||
self.rowId = -1
|
||||
self.id = -1
|
||||
self.rowId = MessageViewModel.optimisticUpdateId
|
||||
self.id = MessageViewModel.optimisticUpdateId
|
||||
self.openGroupServerMessageId = nil
|
||||
self.variant = .standardOutgoing
|
||||
self.timestampMs = timestampMs
|
||||
|
|
Loading…
Reference in New Issue