diff --git a/LibSession-Util b/LibSession-Util index 8d944ab0f..f0016f512 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit 8d944ab0fbab1c5f0fe8861285daffcda7faf9c5 +Subproject commit f0016f512956c3f8fef83e3daf17e76fec272968 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 87d3a5a0e..6a7cf6647 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -5667,7 +5667,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\nENTITLEMENTS_FILE=\"${SRCROOT}/${CODE_SIGN_ENTITLEMENTS}\"\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :AppGroupsId' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :AppGroupsId string\" \"${INFO_PLIST}\"\nfi\n\napp_group_id=$(xmllint --xpath 'string(//key[.=\"com.apple.security.application-groups\"]/following-sibling::array/string)' \"$ENTITLEMENTS_FILE\")\n\n/usr/libexec/PlistBuddy -c \"Set :AppGroupsId ${app_group_id}\" \"${INFO_PLIST}\"\n"; + shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\nENTITLEMENTS_FILE=\"${SRCROOT}/${CODE_SIGN_ENTITLEMENTS}\"\n\n# Wait for Info.plist to be processed\nwhile [[ ! -f \"${INFO_PLIST}\" ]] && [[ ! -f \"${ENTITLEMENTS_FILE}\" ]]; do\n echo \"Waiting for plist and entitlements to be copied\"\n sleep 1\ndone\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :AppGroupsId' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :AppGroupsId string\" \"${INFO_PLIST}\"\nfi\n\napp_group_id=$(xmllint --xpath 'string(//key[.=\"com.apple.security.application-groups\"]/following-sibling::array/string)' \"$ENTITLEMENTS_FILE\")\n\n/usr/libexec/PlistBuddy -c \"Set :AppGroupsId ${app_group_id}\" \"${INFO_PLIST}\"\n"; }; FD5E93D42C12D3CB0038C25A /* Add Commit Hash To Build Info Plist */ = { isa = PBXShellScriptBuildPhase; @@ -5685,7 +5685,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :GitCommitHash' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :GitCommitHash string\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :GitCommitHash `git rev-parse --short=7 HEAD`\" \"${INFO_PLIST}\"\n"; + shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\n\n# Wait for Info.plist to be processed\nwhile [[ ! -f \"${INFO_PLIST}\" ]]; do\n echo \"Waiting for plist to be copied\"\n sleep 1\ndone\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :GitCommitHash' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :GitCommitHash string\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :GitCommitHash `git rev-parse --short=7 HEAD`\" \"${INFO_PLIST}\"\n"; showEnvVarsInLog = 0; }; FD5E93D52C12D8120038C25A /* Add Commit Hash To Build Info Plist */ = { @@ -5704,7 +5704,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :GitCommitHash' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :GitCommitHash string\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :GitCommitHash `git rev-parse --short=7 HEAD`\" \"${INFO_PLIST}\"\n"; + shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\n\n# Wait for Info.plist to be processed\nwhile [[ ! -f \"${INFO_PLIST}\" ]]; do\n echo \"Waiting for plist to be copied\"\n sleep 1\ndone\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :GitCommitHash' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :GitCommitHash string\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :GitCommitHash `git rev-parse --short=7 HEAD`\" \"${INFO_PLIST}\"\n"; showEnvVarsInLog = 0; }; FD5E93D62C12D8270038C25A /* Add App Group To Build Info Plist */ = { @@ -5723,7 +5723,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\nENTITLEMENTS_FILE=\"${SRCROOT}/${CODE_SIGN_ENTITLEMENTS}\"\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :AppGroupsId' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :AppGroupsId string\" \"${INFO_PLIST}\"\nfi\n\napp_group_id=$(xmllint --xpath 'string(//key[.=\"com.apple.security.application-groups\"]/following-sibling::array/string)' \"$ENTITLEMENTS_FILE\")\n\n/usr/libexec/PlistBuddy -c \"Set :AppGroupsId ${app_group_id}\" \"${INFO_PLIST}\"\n"; + shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\nENTITLEMENTS_FILE=\"${SRCROOT}/${CODE_SIGN_ENTITLEMENTS}\"\n\n# Wait for Info.plist to be processed\nwhile [[ ! -f \"${INFO_PLIST}\" ]] && [[ ! -f \"${ENTITLEMENTS_FILE}\" ]]; do\n echo \"Waiting for plist and entitlements to be copied\"\n sleep 1\ndone\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :AppGroupsId' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :AppGroupsId string\" \"${INFO_PLIST}\"\nfi\n\napp_group_id=$(xmllint --xpath 'string(//key[.=\"com.apple.security.application-groups\"]/following-sibling::array/string)' \"$ENTITLEMENTS_FILE\")\n\n/usr/libexec/PlistBuddy -c \"Set :AppGroupsId ${app_group_id}\" \"${INFO_PLIST}\"\n"; }; FD5E93D72C12DA400038C25A /* Add App Group To Build Info Plist */ = { isa = PBXShellScriptBuildPhase; @@ -5741,7 +5741,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\nENTITLEMENTS_FILE=\"${SRCROOT}/${CODE_SIGN_ENTITLEMENTS}\"\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :AppGroupsId' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :AppGroupsId string\" \"${INFO_PLIST}\"\nfi\n\napp_group_id=$(xmllint --xpath 'string(//key[.=\"com.apple.security.application-groups\"]/following-sibling::array/string)' \"$ENTITLEMENTS_FILE\")\n\n/usr/libexec/PlistBuddy -c \"Set :AppGroupsId ${app_group_id}\" \"${INFO_PLIST}\"\n"; + shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\nENTITLEMENTS_FILE=\"${SRCROOT}/${CODE_SIGN_ENTITLEMENTS}\"\n\n# Wait for Info.plist to be processed\nwhile [[ ! -f \"${INFO_PLIST}\" ]] && [[ ! -f \"${ENTITLEMENTS_FILE}\" ]]; do\n echo \"Waiting for plist and entitlements to be copied\"\n sleep 1\ndone\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :AppGroupsId' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :AppGroupsId string\" \"${INFO_PLIST}\"\nfi\n\napp_group_id=$(xmllint --xpath 'string(//key[.=\"com.apple.security.application-groups\"]/following-sibling::array/string)' \"$ENTITLEMENTS_FILE\")\n\n/usr/libexec/PlistBuddy -c \"Set :AppGroupsId ${app_group_id}\" \"${INFO_PLIST}\"\n"; showEnvVarsInLog = 0; }; FD9BDDFF2A5D229B005F1EBC /* Build libSessionUtil if Needed */ = { @@ -5843,7 +5843,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :GitCommitHash' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :GitCommitHash string\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :GitCommitHash `git rev-parse --short=7 HEAD`\" \"${INFO_PLIST}\"\n"; + shellScript = "INFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\n\n# Wait for Info.plist to be processed\nwhile [[ ! -f \"${INFO_PLIST}\" ]]; do\n echo \"Waiting for plist to be copied\"\n sleep 1\ndone\n\n# Query and save the value; suppress any error message, if key not found.\nvalue=$(/usr/libexec/PlistBuddy -c 'print :GitCommitHash' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$value\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :GitCommitHash string\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :GitCommitHash `git rev-parse --short=7 HEAD`\" \"${INFO_PLIST}\"\n"; }; FDE7214D287E50820093DF33 /* Lint Localizable.strings */ = { isa = PBXShellScriptBuildPhase; @@ -8109,7 +8109,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 459; + CURRENT_PROJECT_VERSION = 468; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8146,7 +8146,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.7.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -8187,7 +8187,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 459; + CURRENT_PROJECT_VERSION = 468; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -8219,7 +8219,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.6.3; + MARKETING_VERSION = 2.7.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8250,7 +8250,6 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 461; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -8288,7 +8287,6 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.7.0; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -8322,7 +8320,6 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 461; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -8360,7 +8357,6 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.7.0; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 068b9acb5..83cfde843 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -560,7 +560,11 @@ extension ConversationVC: if self?.viewModel.threadData.threadShouldBeVisible == false { _ = try SessionThread .filter(id: threadId) - .updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true)) + .updateAllAndConfig( + db, + SessionThread.Columns.shouldBeVisible.set(to: true), + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + ) } // Insert the interaction and associated it with the optimistically inserted message so @@ -1123,7 +1127,9 @@ extension ConversationVC: attachment.isMicrosoftDoc || attachment.contentType == OWSMimeTypeApplicationPdf { - + // FIXME: If given an invalid text file (eg with binary data) this hangs forever + // Note: I tried dispatching after a short delay, detecting that the new UI is invalid and dismissing it + // if so but the dismissal didn't work (we may have to wait on Apple to handle this one) let interactionController: UIDocumentInteractionController = UIDocumentInteractionController(url: fileUrl) interactionController.delegate = self interactionController.presentPreview(animated: true) diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index d85d0c10a..af3560ef3 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -112,6 +112,8 @@ struct LoadingScreen: View { self.percentage = 1 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.flow.completeRegistration() + let homeVC: HomeVC = HomeVC(flow: self.flow) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) } diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index ee66f00f3..1e14a38e6 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -130,10 +130,7 @@ struct PNModeScreen: View { // If we are registering then we can just continue on guard flow != .register else { - self.flow.completeRegistration() - self.finishRegister() - - return + return finishRegister() } // Check if we already have a profile name (ie. profile retrieval completed while waiting on @@ -149,12 +146,7 @@ struct PNModeScreen: View { guard existingProfileName?.isEmpty != false else { // If we have one then we can go straight to the home screen - self.flow.completeRegistration() - - // Go to the home screen - let homeVC: HomeVC = HomeVC() - self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) - return + return finishRegister() } // If we don't have one then show a loading indicator and try to retrieve the existing name @@ -166,6 +158,8 @@ struct PNModeScreen: View { } private func finishRegister() { + self.flow.completeRegistration() + let homeVC: HomeVC = HomeVC(flow: self.flow) self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) return diff --git a/Session/Shared/Types/ObservableTableSource.swift b/Session/Shared/Types/ObservableTableSource.swift index b4d85d254..7ec6abf90 100644 --- a/Session/Shared/Types/ObservableTableSource.swift +++ b/Session/Shared/Types/ObservableTableSource.swift @@ -186,14 +186,17 @@ public enum ObservationBuilder { return TableObservation { viewModel, dependencies in subject .withPrevious(([], StagedChangeset())) - .filter { prev, next in - /// Suppress events with no changes (these will be sent in order to clear out the `StagedChangeset` value as if we - /// don't do so then resubscribing will result in an attempt to apply an invalid changeset to the `tableView` resulting - /// in a crash) - !next.1.isEmpty - } + .handleEvents( + receiveCancel: { + /// When we unsubscribe we send through the existing data but clear out the `StagedChangeset` value + /// so that resubscribing doesn't result in the UI trying to reapply the same changeset (which would cause a + /// crash due to invalid table view changes) + subject.send((subject.value.0, StagedChangeset())) + } + ) .map { _, current -> ([T], StagedChangeset<[T]>) in current } .setFailureType(to: Error.self) + .shareReplay(1) .eraseToAnyPublisher() } } diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index fc20532cb..e3b8a568c 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -203,6 +203,11 @@ public class SessionCell: UITableViewCell { botSeparatorLeftConstraint = botSeparator.pin(.left, to: .left, of: cellBackgroundView) botSeparatorRightConstraint = botSeparator.pin(.right, to: .right, of: cellBackgroundView) botSeparator.pin(.bottom, to: .bottom, of: cellBackgroundView) + + // Explicitly call this to ensure we have initialised the constraints before we initially + // layout (if we don't do this then some constraints get created for the first time when + // updating the cell before the `isActive` value gets set, resulting in breaking constriants) + prepareForReuse() } public override func layoutSubviews() { diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index b55d160a4..2a6785c92 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -115,7 +115,7 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, public init( id: String, variant: Variant, - creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), + creationDateTimestamp: TimeInterval, shouldBeVisible: Bool = false, isPinned: Bool = false, messageDraft: String? = nil, @@ -158,12 +158,14 @@ public extension SessionThread { _ db: Database, id: ID, variant: Variant, + creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), shouldBeVisible: Bool? ) throws -> SessionThread { guard let existingThread: SessionThread = try? fetchOne(db, id: id) else { return try SessionThread( id: id, variant: variant, + creationDateTimestamp: creationDateTimestamp, shouldBeVisible: (shouldBeVisible ?? false) ).saved(db) } @@ -187,8 +189,12 @@ public extension SessionThread { // would result in an infinite loop) return (try fetchOne(db, id: id)) .defaulting( - to: try SessionThread(id: id, variant: variant, shouldBeVisible: desiredVisibility) - .saved(db) + to: try SessionThread( + id: id, + variant: variant, + creationDateTimestamp: creationDateTimestamp, + shouldBeVisible: desiredVisibility + ).saved(db) ) } diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index e70b578c1..df54e81b6 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -28,9 +28,15 @@ public enum MessageSendJob: JobExecutor { return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) } - // We need to include 'fileIds' when sending messages with attachments to Open Groups - // so extract them from any associated attachments + /// We need to include `fileIds` when sending messages with attachments to Open Groups so extract them from any + /// associated attachments var messageFileIds: [String] = [] + let messageType: String = { + switch details.destination { + case .syncMessage: return "\(type(of: details.message)) (SyncMessage)" + default: return "\(type(of: details.message))" + } + }() /// Ensure any associated attachments have already been uploaded before sending the message /// @@ -188,11 +194,11 @@ public enum MessageSendJob: JobExecutor { receiveCompletion: { result in switch result { case .finished: - Log.info("[MessageSendJob] Completed sending \(type(of: details.message)) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s).") + Log.info("[MessageSendJob] Completed sending \(messageType) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s).") success(job, false, dependencies) case .failure(let error): - Log.info("[MessageSendJob] Failed to send \(type(of: details.message)) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s) due to error: \(error).") + Log.info("[MessageSendJob] Failed to send \(messageType) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s) due to error: \(error).") // Actual error handling switch (error, details.message) { @@ -266,42 +272,9 @@ extension MessageSendJob { throw StorageError.decodingFailed } - let message: Message = try variant.decode(from: container, forKey: .message) - var destination: Message.Destination = try container.decode(Message.Destination.self, forKey: .destination) - - /// Handle the legacy 'isSyncMessage' flag - this flag was deprecated in `2.5.2` (April 2024) and can be removed in a - /// subsequent release after May 2024 - if ((try? container.decode(Bool.self, forKey: .isSyncMessage)) ?? false) { - switch (destination, message) { - case (.contact, let message as VisibleMessage): - guard let targetPublicKey: String = message.syncTarget else { - SNLog("Unable to decode messageSend job due to missing syncTarget") - throw StorageError.decodingFailed - } - - destination = .syncMessage(originalRecipientPublicKey: targetPublicKey) - - case (.contact, let message as ExpirationTimerUpdate): - guard let targetPublicKey: String = message.syncTarget else { - SNLog("Unable to decode messageSend job due to missing syncTarget") - throw StorageError.decodingFailed - } - - destination = .syncMessage(originalRecipientPublicKey: targetPublicKey) - - case (.contact(let publicKey), _): - SNLog("Sync message in messageSend job was missing explicit syncTarget (falling back to specified value)") - destination = .syncMessage(originalRecipientPublicKey: publicKey) - - default: - SNLog("Unable to decode messageSend job due to invalid sync message state") - throw StorageError.decodingFailed - } - } - self = Details( - destination: destination, - message: message + destination: try container.decode(Message.Destination.self, forKey: .destination), + message: try variant.decode(from: container, forKey: .message) ) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index 498496242..5e64ba07c 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -9,6 +9,14 @@ import SessionUtilitiesKit // MARK: - Convenience +public extension LibSession { + /// A `0` `priority` value indicates visible, but not pinned + static let visiblePriority: Int32 = 0 + + /// A negative `priority` value indicates hidden + static let hiddenPriority: Int32 = -1 +} + internal extension LibSession { /// This is a buffer period within which we will process messages which would result in a config change, any message which would normally /// result in a config change which was sent before `lastConfigMessage.timestamp - configChangeBufferPeriod` will not @@ -34,12 +42,6 @@ internal extension LibSession { return !allColumnsThatTriggerConfigUpdate.isDisjoint(with: targetColumns) } - /// A `0` `priority` value indicates visible, but not pinned - static let visiblePriority: Int32 = 0 - - /// A negative `priority` value indicates hidden - static let hiddenPriority: Int32 = -1 - static func shouldBeVisible(priority: Int32) -> Bool { return (priority >= LibSession.visiblePriority) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 84e61ee89..38abd6480 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -217,10 +217,24 @@ internal extension LibSession { let name: String = group.name, let lastKeyPair: ClosedGroupKeyPair = group.lastKeyPair, let members: [GroupMember] = group.groupMembers, - let updatedAdmins: Set = group.groupAdmins?.asSet(), - let joinedAt: Int64 = group.joinedAt + let updatedAdmins: Set = group.groupAdmins?.asSet() else { return } + // There were some bugs (somewhere) where the `joinedAt` valid could be in seconds, milliseconds + // or even microseconds so we need to try to detect this and convert it to proper seconds (if we don't + // have a value then we want it to be at the bottom of the list, so default to `0`) + let joinedAt: TimeInterval = { + guard let value: Int64 = group.joinedAt else { return 0 } + + if value > 9_000_000_000_000 { // Microseconds + return (Double(value) / 1_000_000) + } else if value > 9_000_000_000 { // Milliseconds + return (Double(value) / 1000) + } + + return TimeInterval(value) // Seconds + }() + if !existingLegacyGroupIds.contains(group.id) { // Add a new group if it doesn't already exist try MessageReceiver.handleNewClosedGroup( @@ -237,7 +251,7 @@ internal extension LibSession { .map { $0.profileId }, admins: updatedAdmins.map { $0.profileId }, expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0), - formationTimestampMs: UInt64((group.joinedAt.map { $0 * 1000 } ?? latestConfigSentTimestampMs)), + formationTimestampMs: UInt64(joinedAt * 1000), calledFromConfigHandling: true, using: dependencies ) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index cca882809..02275f9a0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -865,6 +865,16 @@ public enum OpenGroupAPI { .signed(db, server: openGroup.server, data: data, using: dependencies) } + public static func isInvalidOpenGroupFileUrl(url: URL, openGroup: OpenGroup) -> Bool { + // If the url doesn't end with a fileId then it's invalid + guard let fileId: String = Attachment.fileId(for: url.absoluteString) else { return true } + + return ( + !url.absoluteString.starts(with: openGroup.server) || + url != (try? downloadUrlFor(fileId: fileId, server: openGroup.server, roomToken: openGroup.roomToken)) + ) + } + public static func downloadUrlFor( fileId: String, server: String, @@ -900,7 +910,10 @@ public enum OpenGroupAPI { url: try { // FIXME: Remove this logic once the 'downloadUrl' for SOGS is being set correctly guard - Network.isFileServerUrl(url: url), + ( + isInvalidOpenGroupFileUrl(url: url, openGroup: openGroup) || + Network.isFileServerUrl(url: url) + ), let fileId: String = Attachment.fileId(for: url.absoluteString) else { return url } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 7a9374ffd..ce51e65f4 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -153,7 +153,13 @@ extension MessageReceiver { // Create the group let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup, shouldBeVisible: true) + .fetchOrCreate( + db, + id: groupPublicKey, + variant: .legacyGroup, + creationDateTimestamp: (TimeInterval(formationTimestampMs) / 1000), + shouldBeVisible: true + ) let closedGroup: ClosedGroup = try ClosedGroup( threadId: groupPublicKey, name: name, diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index 728c203a6..141828a5c 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -170,24 +170,32 @@ extension MessageSender { attachments .map { attachment -> AnyPublisher in attachment - .upload( - to: ( - openGroup.map { Attachment.Destination.openGroup($0) } ?? - .fileServer - ), - using: dependencies - ) + .upload(to: (openGroup.map { .openGroup($0) } ?? .fileServer), using: dependencies) } ) .collect() .eraseToAnyPublisher() } - .map { results -> PreparedSendData in + .flatMap { results -> AnyPublisher in // Once the attachments are processed then update the PreparedSendData with // the fileIds associated to the message let fileIds: [String] = results.compactMap { result -> String? in Attachment.fileId(for: result) } - return preparedSendData.with(fileIds: fileIds) + // We need to regenerate the 'PreparedSendData' because otherwise the `SnodeMessage` won't + // contain the attachment data + return dependencies.storage + .writePublisher { db in + try MessageSender.preparedSendData( + db, + message: preparedSendData.message, + to: preparedSendData.destination, + namespace: preparedSendData.destination.defaultNamespace, + interactionId: preparedSendData.interactionId, + using: dependencies + ) + } + .map { updatedData -> PreparedSendData in updatedData.with(fileIds: fileIds) } + .eraseToAnyPublisher() } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 88951cd4e..66785fa69 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -15,7 +15,7 @@ public final class MessageSender { let destination: Message.Destination let namespace: SnodeAPI.Namespace? - let message: Message? + let message: Message let interactionId: Int64? let totalAttachmentsUploaded: Int @@ -25,7 +25,7 @@ public final class MessageSender { private init( shouldSend: Bool, - message: Message?, + message: Message, destination: Message.Destination, namespace: SnodeAPI.Namespace?, interactionId: Int64?, @@ -621,17 +621,15 @@ public final class MessageSender { guard expectedAttachmentUploadCount == data.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 = data.message { - dependencies.storage.read { db in - MessageSender.handleFailedMessageSend( - db, - message: message, - destination: data.destination, - with: .attachmentsNotUploaded, - interactionId: data.interactionId, - using: dependencies - ) - } + dependencies.storage.read { db in + MessageSender.handleFailedMessageSend( + db, + message: data.message, + destination: data.destination, + with: .attachmentsNotUploaded, + interactionId: data.interactionId, + using: dependencies + ) } return Fail(error: MessageSenderError.attachmentsNotUploaded) @@ -657,7 +655,6 @@ public final class MessageSender { using dependencies: Dependencies ) -> AnyPublisher { guard - let message: Message = data.message, let namespace: SnodeAPI.Namespace = data.namespace, let snodeMessage: SnodeMessage = data.snodeMessage else { @@ -668,7 +665,7 @@ public final class MessageSender { return dependencies.network .send(.message(snodeMessage, in: namespace), using: dependencies) .flatMap { info, response -> AnyPublisher in - let updatedMessage: Message = message + let updatedMessage: Message = data.message updatedMessage.serverHash = response.hash // Only legacy groups need to manually trigger push notifications now so only create the job @@ -744,7 +741,7 @@ public final class MessageSender { dependencies.storage.read { db in MessageSender.handleFailedMessageSend( db, - message: message, + message: data.message, destination: data.destination, with: .other(error), interactionId: data.interactionId, @@ -765,7 +762,6 @@ public final class MessageSender { using dependencies: Dependencies ) -> AnyPublisher { guard - let message: Message = data.message, case .openGroup(let roomToken, let server, let whisperTo, let whisperMods, let fileIds) = data.destination, let plaintext: Data = data.plaintext else { @@ -791,7 +787,7 @@ public final class MessageSender { .flatMap { $0.send(using: dependencies) } .flatMap { (responseInfo, responseData) -> AnyPublisher in let serverTimestampMs: UInt64? = responseData.posted.map { UInt64(floor($0 * 1000)) } - let updatedMessage: Message = message + let updatedMessage: Message = data.message updatedMessage.openGroupServerMessageId = UInt64(responseData.id) return dependencies.storage.writePublisher { db in @@ -816,7 +812,7 @@ public final class MessageSender { dependencies.storage.read { db in MessageSender.handleFailedMessageSend( db, - message: message, + message: data.message, destination: data.destination, with: .other(error), interactionId: data.interactionId, @@ -834,7 +830,6 @@ public final class MessageSender { using dependencies: Dependencies ) -> AnyPublisher { guard - let message: Message = data.message, case .openGroupInbox(let server, _, let recipientBlindedPublicKey) = data.destination, let ciphertext: Data = data.ciphertext else { @@ -856,7 +851,7 @@ public final class MessageSender { } .flatMap { $0.send(using: dependencies) } .flatMap { (responseInfo, responseData) -> AnyPublisher in - let updatedMessage: Message = message + let updatedMessage: Message = data.message updatedMessage.openGroupServerMessageId = UInt64(responseData.id) return dependencies.storage.writePublisher { db in @@ -881,7 +876,7 @@ public final class MessageSender { dependencies.storage.read { db in MessageSender.handleFailedMessageSend( db, - message: message, + message: data.message, destination: data.destination, with: .other(error), interactionId: data.interactionId, diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index d5b0f7e36..c055fc4c0 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -37,7 +37,8 @@ class OpenGroupManagerSpec: QuickSpec { ) @TestState var testGroupThread: SessionThread! = SessionThread( id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), - variant: .community + variant: .community, + creationDateTimestamp: 0 ) @TestState var testOpenGroup: OpenGroup! = OpenGroup( server: "testServer", diff --git a/SessionNotificationServiceExtension/NotificationError.swift b/SessionNotificationServiceExtension/NotificationError.swift index 8f16548fc..355c5563c 100644 --- a/SessionNotificationServiceExtension/NotificationError.swift +++ b/SessionNotificationServiceExtension/NotificationError.swift @@ -17,8 +17,8 @@ enum NotificationError: Error, CustomStringConvertible { case .processing(let result): return "Failed to process notification (\(result)) (NotificationError.processing)." case .messageProcessing: return "Failed to process message (NotificationError.messageProcessing)." case .ignorableMessage: return "Ignorable message (NotificationError.ignorableMessage)." - case .messageHandling(let error): return "Failed to handle message (\("\(error)".noPeriod)) (NotificationError.messageHandling)." - case .other(let error): return "Unknown error occurred: \("\(error)".noPeriod)) (NotificationError.other)." + case .messageHandling(let error): return "Failed to handle message (\(error)) (NotificationError.messageHandling)." + case .other(let error): return "Unknown error occurred: \(error) (NotificationError.other)." } } } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 7dd57b1d2..bda9b6fad 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -249,13 +249,20 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .flatMap { _ in dependencies.storage.writePublisher { db -> MessageSender.PreparedSendData in - guard - let threadVariant: SessionThread.Variant = try SessionThread + guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { + throw MessageSenderError.noThread + } + + // Update the thread to be visible (if it isn't already) + if !thread.shouldBeVisible || thread.pinnedPriority == LibSession.hiddenPriority { + _ = try SessionThread .filter(id: threadId) - .select(.variant) - .asRequest(of: SessionThread.Variant.self) - .fetchOne(db) - else { throw MessageSenderError.noThread } + .updateAllAndConfig( + db, + SessionThread.Columns.shouldBeVisible.set(to: true), + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + ) + } // Create the interaction let interaction: Interaction = try Interaction( diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionSnodeKit/LibSession/LibSession+Networking.swift index 0c6173c4a..53b6e30af 100644 --- a/SessionSnodeKit/LibSession/LibSession+Networking.swift +++ b/SessionSnodeKit/LibSession/LibSession+Networking.swift @@ -36,12 +36,17 @@ public extension LibSession { static var hasPaths: Bool { !lastPaths.wrappedValue.isEmpty } static var pathsDescription: String { lastPaths.wrappedValue.prettifiedDescription } - fileprivate class CallbackWrapper { + internal class CallbackWrapper { public let resultPublisher: CurrentValueSubject = CurrentValueSubject(nil) + private let callback: ((Output) -> Void)? private var pointersToDeallocate: [UnsafeRawPointer?] = [] // MARK: - Initialization + init(_ callback: ((Output) -> Void)? = nil) { + self.callback = callback + } + deinit { pointersToDeallocate.forEach { $0?.deallocate() } } @@ -75,9 +80,17 @@ public extension LibSession { return Log.error("[LibSession] CallbackWrapper called with null context.") } - // Dispatch async so we don't block libSession's internals with Swift logic (which can block other requests) let wrapper: CallbackWrapper = Unmanaged>.fromOpaque(ctx).takeRetainedValue() - DispatchQueue.global(qos: .default).async { [wrapper] in wrapper.resultPublisher.send(output) } + + switch wrapper.callback { + case .none: + // Dispatch async so we don't block libSession's internals with Swift logic (which can block other requests) + DispatchQueue.global(qos: .default).async { [wrapper] in wrapper.resultPublisher.send(output) } + + case .some(let callback): + // We generally shouldn't use the `callback` method but it's useful for tests + callback(output) + } } public func unsafePointer() -> UnsafeMutableRawPointer { Unmanaged.passRetained(self).toOpaque() } @@ -193,7 +206,7 @@ public extension LibSession { guard nodesSize > 0, let cSwarm: UnsafeMutablePointer = nodesPtr - else { return CallbackWrapper.run(ctx, .failure(SnodeAPIError.unableToRetrieveSwarm)) } + else { return CallbackWrapper.run(ctx, .failure(SnodeAPIError.ranOutOfRandomSnodes(nil))) } var nodes: Set = [] (0..= count else { throw SnodeAPIError.unableToRetrieveSwarm } + guard nodes.count >= count else { throw SnodeAPIError.ranOutOfRandomSnodes(nil) } return nodes } @@ -401,6 +414,10 @@ public extension LibSession { Log.warn("[LibSession] Attempted to access suspended network.") return Fail(error: NetworkError.suspended).eraseToAnyPublisher() } + guard Singleton.hasAppContext && (Singleton.appContext.isMainApp || Singleton.appContext.isShareExtension) else { + Log.warn("[LibSession] Attempted to create network in invalid extension.") + return Fail(error: NetworkError.suspended).eraseToAnyPublisher() + } guard networkCache.wrappedValue == nil else { return Just(networkCache.wrappedValue) @@ -606,6 +623,19 @@ extension LibSession { ed25519PubkeyHex = String(libSessionVal: cSnode.ed25519_pubkey_hex) } + internal init?(nodeString: String) { + let parts: [String] = nodeString.components(separatedBy: "|") + + guard + parts.count == 4, + let port: UInt16 = UInt16(parts[1]) + else { return nil } + + ip = parts[0] + quicPort = port + ed25519PubkeyHex = parts[3] + } + public func hash(into hasher: inout Hasher) { ip.hash(into: &hasher) quicPort.hash(into: &hasher) @@ -642,7 +672,7 @@ public extension Network.Destination { } } -private extension LibSession.CallbackWrapper { +internal extension LibSession.CallbackWrapper { func cServerDestination(_ destination: Network.Destination) throws -> network_server_destination { guard case .server(let url, let method, let headers, let x25519PublicKey) = destination, diff --git a/SessionSnodeKit/Types/SnodeAPIError.swift b/SessionSnodeKit/Types/SnodeAPIError.swift index b101cf2c6..a9ed6a7f0 100644 --- a/SessionSnodeKit/Types/SnodeAPIError.swift +++ b/SessionSnodeKit/Types/SnodeAPIError.swift @@ -57,9 +57,7 @@ public enum SnodeAPIError: Error, CustomStringConvertible { case .ranOutOfRandomSnodes(let maybeError): switch maybeError { case .none: return "Ran out of random snodes (SnodeAPIError.ranOutOfRandomSnodes(nil))." - case .some(let error): - let errorDesc = "\(error)".trimmingCharacters(in: CharacterSet(["."])) - return "Ran out of random snodes (SnodeAPIError.ranOutOfRandomSnodes(\(errorDesc))." + case .some(let error): return "Ran out of random snodes (SnodeAPIError.ranOutOfRandomSnodes(\(error))." } // ONS diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index edf5519a1..9306d9e13 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -30,7 +30,8 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { initialData: { db in try SessionThread( id: "TestId", - variant: .contact + variant: .contact, + creationDateTimestamp: 0 ).insert(db) }, using: dependencies diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index db68bd1d0..fff799427 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -41,7 +41,7 @@ class ThreadSettingsViewModelSpec: QuickSpec { data: Data(hex: TestConstants.publicKey) ).insert(db) - try SessionThread(id: "TestId",variant: .contact).insert(db) + try SessionThread(id: "TestId",variant: .contact, creationDateTimestamp: 0).insert(db) try Profile(id: "05\(TestConstants.publicKey)", name: "TestMe").insert(db) try Profile(id: "TestId", name: "TestUser").insert(db) }, @@ -142,7 +142,8 @@ class ThreadSettingsViewModelSpec: QuickSpec { try SessionThread( id: "05\(TestConstants.publicKey)", - variant: .contact + variant: .contact, + creationDateTimestamp: 0 ).insert(db) } @@ -306,7 +307,8 @@ class ThreadSettingsViewModelSpec: QuickSpec { try SessionThread( id: "TestId", - variant: .contact + variant: .contact, + creationDateTimestamp: 0 ).insert(db) } } @@ -439,7 +441,8 @@ class ThreadSettingsViewModelSpec: QuickSpec { try SessionThread( id: "TestId", - variant: .legacyGroup + variant: .legacyGroup, + creationDateTimestamp: 0 ).insert(db) } @@ -484,7 +487,8 @@ class ThreadSettingsViewModelSpec: QuickSpec { try SessionThread( id: "TestId", - variant: .community + variant: .community, + creationDateTimestamp: 0 ).insert(db) } diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index 9b6f6dd3a..0856fc338 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -409,7 +409,6 @@ public class PagedDatabaseObserver: TransactionObserver where updatedPageInfo.totalCount + changesToQuery .filter { $0.kind == .insert } - .filter { validChangeRowIds.contains($0.rowId) } .count ) ) @@ -1085,10 +1084,7 @@ public enum PagedData { // No need to do anything if there were no changes guard !changeset.isEmpty else { return } - // Need to send an event with the changes and then a second event to clear out the `StagedChangeset` - // value otherwise resubscribing will result with the changes coming through a second time valueSubject?.send((updatedData, changeset)) - valueSubject?.send((updatedData, StagedChangeset())) } // No need to dispatch to the next run loop if we are already on the main thread diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index 895fe6df6..b6faf4b86 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -77,14 +77,6 @@ public extension String { // MARK: - Formatting -public extension String { - var noPeriod: String { - guard self.hasSuffix(".") && !self.hasSuffix("...") else { return self } - - return String(self.prefix(count - 1)) - } -} - public extension String.StringInterpolation { mutating func appendInterpolation(_ value: TimeUnit, unit: TimeUnit.Unit, resolution: Int = 2) { appendLiteral("\(TimeUnit(value, unit: unit, resolution: resolution))") // stringlint:disable diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 811a8aa70..af0beaad7 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -742,7 +742,7 @@ public final class JobRunner: JobRunnerType { ) -> (Int64, Job)? { switch job?.behaviour { case .recurringOnActive, .recurringOnLaunch, .runOnceNextLaunch: - SNLog("[JobRunner] Attempted to insert \(job?.variant) job before the current one even though it's behaviour is \(job?.behaviour)") + SNLog("[JobRunner] Attempted to insert \(job) before the current one even though it's behaviour is \(job?.behaviour)") return nil default: break @@ -843,7 +843,7 @@ public final class JobRunner: JobRunnerType { case (.enqueueOnly, .some(let uniqueHashValue)): // Nothing currently running or sitting in a JobQueue guard !allJobInfo().contains(where: { _, info -> Bool in info.uniqueHashValue == uniqueHashValue }) else { - SNLog("[JobRunner] Unable to add \(job.variant) job due to unique constraint") + SNLog("[JobRunner] Unable to add \(job) due to unique constraint") return nil } @@ -856,7 +856,7 @@ public final class JobRunner: JobRunnerType { // Nothing in the database !Job.filter(Job.Columns.uniqueHashValue == uniqueHashValue).isNotEmpty(db) else { - SNLog("[JobRunner] Unable to add \(job.variant) job due to unique constraint") + SNLog("[JobRunner] Unable to add \(job) due to unique constraint") return nil } @@ -864,7 +864,7 @@ public final class JobRunner: JobRunnerType { case (.persist, .none): guard let updatedJob: Job = try? job.inserted(db), updatedJob.id != nil else { - SNLog("[JobRunner] Unable to add \(job.variant) job\(job.id == nil ? " due to missing id" : "")") + SNLog("[JobRunner] Unable to add \(job)\(job.id == nil ? " due to missing id" : "")") return nil } @@ -1062,7 +1062,7 @@ public final class JobQueue: Hashable { job.nextRunTimestamp <= dependencies.dateNow.timeIntervalSince1970 else { return } guard job.id != nil else { - SNLog("[JobRunner] Prevented attempt to add \(job.variant) job without id to queue") + SNLog("[JobRunner] Prevented attempt to add \(job) without id to queue") return } @@ -1090,7 +1090,7 @@ public final class JobQueue: Hashable { using dependencies: Dependencies ) { guard let jobId: Int64 = job.id else { - SNLog("[JobRunner] Prevented attempt to upsert \(job.variant) job without id to queue") + SNLog("[JobRunner] Prevented attempt to upsert \(job) without id to queue") return } @@ -1116,7 +1116,7 @@ public final class JobQueue: Hashable { fileprivate func insert(_ job: Job, before otherJob: Job) { guard job.id != nil else { - SNLog("[JobRunner] Prevented attempt to insert \(job.variant) job without id to queue") + SNLog("[JobRunner] Prevented attempt to insert \(job) without id to queue") return } @@ -1323,7 +1323,7 @@ public final class JobQueue: Hashable { // Run the first job in the pendingJobsQueue if !wasAlreadyRunning { - Log.info("[JobRunner] Starting \(queueContext) with (\(jobCount) job\(jobCount != 1 ? "s" : ""))") + Log.info("[JobRunner] Starting \(queueContext) with \(jobCount) jobs") } runNextJob(using: dependencies) } @@ -1360,7 +1360,7 @@ public final class JobQueue: Hashable { return } guard let jobExecutor: JobExecutor.Type = executorMap.wrappedValue[nextJob.variant] else { - SNLog("[JobRunner] \(queueContext) Unable to run \(nextJob.variant) job due to missing executor") + SNLog("[JobRunner] \(queueContext) Unable to run \(nextJob) due to missing executor") handleJobFailed( nextJob, error: JobRunnerError.executorMissing, @@ -1370,7 +1370,7 @@ public final class JobQueue: Hashable { return } guard !jobExecutor.requiresThreadId || nextJob.threadId != nil else { - SNLog("[JobRunner] \(queueContext) Unable to run \(nextJob.variant) job due to missing required threadId") + SNLog("[JobRunner] \(queueContext) Unable to run \(nextJob) due to missing required threadId") handleJobFailed( nextJob, error: JobRunnerError.requiredThreadIdMissing, @@ -1380,7 +1380,7 @@ public final class JobQueue: Hashable { return } guard !jobExecutor.requiresInteractionId || nextJob.interactionId != nil else { - SNLog("[JobRunner] \(queueContext) Unable to run \(nextJob.variant) job due to missing required interactionId") + SNLog("[JobRunner] \(queueContext) Unable to run \(nextJob) due to missing required interactionId") handleJobFailed( nextJob, error: JobRunnerError.requiredInteractionIdMissing, @@ -1390,7 +1390,7 @@ public final class JobQueue: Hashable { return } guard nextJob.id != nil else { - SNLog("[JobRunner] \(queueContext) Unable to run \(nextJob.variant) job due to missing id") + SNLog("[JobRunner] \(queueContext) Unable to run \(nextJob) due to missing id") handleJobFailed( nextJob, error: JobRunnerError.jobIdMissing, @@ -1420,7 +1420,7 @@ public final class JobQueue: Hashable { .defaulting(to: (0, [])) guard dependencyInfo.jobs.count == dependencyInfo.expectedCount else { - SNLog("[JobRunner] \(queueContext) Removing \(nextJob.variant) job due to missing dependencies") + SNLog("[JobRunner] \(queueContext) Removing \(nextJob) due to missing dependencies") handleJobFailed( nextJob, error: JobRunnerError.missingDependencies, @@ -1430,7 +1430,7 @@ public final class JobQueue: Hashable { return } guard dependencyInfo.jobs.isEmpty else { - SNLog("[JobRunner] \(queueContext) Deferring \(nextJob.variant) job until \(dependencyInfo.jobs.count) dependenc\(dependencyInfo.jobs.count == 1 ? "y" : "ies") are completed") + SNLog("[JobRunner] \(queueContext) Deferring \(nextJob) until \(dependencyInfo.jobs.count) dependencies are completed") // Enqueue the dependencies then defer the current job dependencies.jobRunner.enqueueDependenciesIfNeeded( @@ -1467,7 +1467,7 @@ public final class JobQueue: Hashable { ) ) } - SNLog("[JobRunner] \(queueContext) started \(nextJob.variant) job (\(executionType == .concurrent ? "\(numJobsRunning) currently running, " : "")\(numJobsRemaining) remaining)") + SNLog("[JobRunner] \(queueContext) started \(nextJob) (\(executionType == .concurrent ? "\(numJobsRunning) currently running, " : "")\(numJobsRemaining) remaining)") jobExecutor.run( nextJob, @@ -1642,7 +1642,7 @@ public final class JobQueue: Hashable { using dependencies: Dependencies ) { guard dependencies.storage.read(using: dependencies, { db in try Job.exists(db, id: job.id ?? -1) }) == true else { - SNLog("[JobRunner] \(queueContext) \(job.variant) job canceled") + SNLog("[JobRunner] \(queueContext) \(job) canceled") performCleanUp(for: job, result: .failed(error, permanentFailure), using: dependencies) internalQueue.async(using: dependencies) { [weak self] in @@ -1655,7 +1655,7 @@ public final class JobQueue: Hashable { // immediately (in this case we don't trigger any job callbacks because the // job isn't actually done, it's going to try again immediately) if self.type == .blocking && job.shouldBlock { - SNLog("[JobRunner] \(queueContext) \(job.variant) job failed due to error: \("\(error ?? JobRunnerError.unknown)".noPeriod); retrying immediately") + SNLog("[JobRunner] \(queueContext) \(job) failed due to error: \(error ?? JobRunnerError.unknown); retrying immediately") // If it was a possible deferral loop then we don't actually want to // retry the job (even if it's a blocking one, this gives a small chance @@ -1747,7 +1747,7 @@ public final class JobQueue: Hashable { } } - SNLog("[JobRunner] \(queueContext) \(job.variant) job \(failureText)") + SNLog("[JobRunner] \(queueContext) \(job) \(failureText)") performCleanUp(for: job, result: .failed(error, permanentFailure), using: dependencies) internalQueue.async(using: dependencies) { [weak self] in self?.runNextJob(using: dependencies) @@ -1840,6 +1840,19 @@ public final class JobQueue: Hashable { // MARK: - Formatting +private extension String.StringInterpolation { + mutating func appendInterpolation(_ job: Job) { + appendLiteral("\(job.variant) job (id: \(job.id ?? -1))") // stringlint:disable + } + + mutating func appendInterpolation(_ job: Job?) { + switch job { + case .some(let job): appendInterpolation(job) + case .none: appendLiteral("null job") // stringlint:disable + } + } +} + extension String.StringInterpolation { mutating func appendInterpolation(_ variant: Job.Variant?) { appendLiteral(variant.map { "\($0)" } ?? "unknown") // stringlint:disable