diff --git a/.drone.jsonnet b/.drone.jsonnet index 1d204720a..07634ce4c 100644 --- a/.drone.jsonnet +++ b/.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: '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: [ + './Scripts/drone-static-upload.sh' + ] + }, + ], + }, + // AppStore build (generate an archive to be signed later) { kind: 'pipeline', type: 'exec', - name: 'Test Upload', + 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: [ - './.drone-static-upload.sh' + './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' -// ], -// }, -// ], -// }, ] \ No newline at end of file diff --git a/LibSession-Util b/LibSession-Util index e0b994201..d8f07fa92 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit e0b994201a016cc5bf9065526a0ceb4291f60d5a +Subproject commit d8f07fa92c12c5c2409774e03e03395d7847d1c2 diff --git a/Scripts/LintLocalizableStrings.swift b/Scripts/LintLocalizableStrings.swift index 12bd626aa..910179348 100755 --- a/Scripts/LintLocalizableStrings.swift +++ b/Scripts/LintLocalizableStrings.swift @@ -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 diff --git a/.drone-static-upload.sh b/Scripts/drone-static-upload.sh similarity index 83% rename from .drone-static-upload.sh rename to Scripts/drone-static-upload.sh index b1f7c0887..4fd13faa5 100755 --- a/.drone-static-upload.sh +++ b/Scripts/drone-static-upload.sh @@ -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 diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index db534d40b..e2a78ce64 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -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)) - } - - // 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) + 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 + ) } - - // 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() + } ) - } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .sinkUntilComplete( - receiveCompletion: { [weak self] _ in - self?.handleMessageSent() - } - ) + } } func handleMessageSent() { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index d25342c47..6242fd410 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -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 diff --git a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift index 1a3f6eef5..57ce17769 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift @@ -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 diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index ad8f54c77..a9c4ffb8d 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -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 - } - } - .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 - ) + // 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, [], []) } - .forEach { otherJobId, _ in - // Create the dependency between the jobs - try JobDependencies( - jobId: jobId, - dependantId: otherJobId - ) - .insert(db) + + // 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) } - - // 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 + + /// 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 + } + } + .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 diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 9005bf1b8..2893c8aa1 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -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 } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index 02b793160..3936baf4f 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -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. /// diff --git a/SessionMessagingKit/Sending & Receiving/Quotes/QuotedReplyModel.swift b/SessionMessagingKit/Sending & Receiving/Quotes/QuotedReplyModel.swift index 562621a36..94a6c2ee4 100644 --- a/SessionMessagingKit/Sending & Receiving/Quotes/QuotedReplyModel.swift +++ b/SessionMessagingKit/Sending & Receiving/Quotes/QuotedReplyModel.swift @@ -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 - } -} diff --git a/SessionMessagingKit/SessionUtil/SessionUtil.swift b/SessionMessagingKit/SessionUtil/SessionUtil.swift index 62b77c9fe..d933238a5 100644 --- a/SessionMessagingKit/SessionUtil/SessionUtil.swift +++ b/SessionMessagingKit/SessionUtil/SessionUtil.swift @@ -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 } diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 67d719d74..57fd9acc8 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -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