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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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