diff --git a/LibSession-Util b/LibSession-Util index 6c86cc0d3..c7c68fb6b 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit 6c86cc0d374bf0b3372512c31ab6d0d5727be643 +Subproject commit c7c68fb6b344d431f6a5b7652eab0fd8f7be8286 diff --git a/Podfile b/Podfile index c50d79372..adfc17a42 100644 --- a/Podfile +++ b/Podfile @@ -12,7 +12,7 @@ abstract_target 'GlobalDependencies' do pod 'GRDB.swift/SQLCipher' # FIXME: Would be nice to migrate from CocoaPods to SwiftPackageManager (should allow us to speed up build time), haven't gone through all of the dependencies but currently unfortunately SQLCipher doesn't support SPM (for more info see: https://github.com/sqlcipher/sqlcipher/issues/371) - pod 'SQLCipher', '~> 4.5.3' + pod 'SQLCipher', '~> 4.5.7' pod 'WebRTC-lib' target 'Session' do diff --git a/Podfile.lock b/Podfile.lock index a1305684d..c96d3321c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -11,7 +11,7 @@ PODS: - DifferenceKit/Core (1.3.0) - DifferenceKit/UIKitExtension (1.3.0): - DifferenceKit/Core - - GRDB.swift/SQLCipher (6.13.0): + - GRDB.swift/SQLCipher (6.24.1): - SQLCipher (>= 3.4.2) - libwebp (1.3.2): - libwebp/demux (= 1.3.2) @@ -37,10 +37,10 @@ PODS: - CocoaLumberjack - OpenSSL-Universal - Sodium (0.9.1) - - SQLCipher (4.5.3): - - SQLCipher/standard (= 4.5.3) - - SQLCipher/common (4.5.3) - - SQLCipher/standard (4.5.3): + - SQLCipher (4.5.7): + - SQLCipher/standard (= 4.5.7) + - SQLCipher/common (4.5.7) + - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.5.0) - WebRTC-lib (114.0.0) @@ -60,7 +60,7 @@ DEPENDENCIES: - SAMKeychain - SignalCoreKit (from `https://github.com/oxen-io/session-ios-core-kit`, commit `3acbfe5`) - Sodium (from `https://github.com/oxen-io/session-ios-swift-sodium.git`, commit `310c343`) - - SQLCipher (~> 4.5.3) + - SQLCipher (~> 4.5.7) - SwiftProtobuf (~> 1.5.0) - WebRTC-lib - YYImage/libwebp (from `https://github.com/signalapp/YYImage`) @@ -112,7 +112,7 @@ SPEC CHECKSUMS: CocoaLumberjack: 78abfb691154e2a9df8ded4350d504ee19d90732 Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6 DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca - GRDB.swift: fe420b1af49ec519c7e96e07887ee44f5dfa2b78 + GRDB.swift: 136dcb5d8dddca50aae3ba7d77475f79e7232cd8 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 Nimble: f8a8219d16f176429b951e8f7e72df5c23ceddc0 NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667 @@ -122,11 +122,11 @@ SPEC CHECKSUMS: SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SignalCoreKit: 1fbd8732163ef76de16cd1107d1fa3684b607e5d Sodium: a7d42cb46e789d2630fa552d35870b416ed055ae - SQLCipher: 57fa9f863fa4a3ed9dd3c90ace52315db8c0fdca + SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2 WebRTC-lib: d83df8976fa608b980f1d85796b3de66d60a1953 YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 -PODFILE CHECKSUM: 92bc475070c02411caf98ca5e543dbcb188098e2 +PODFILE CHECKSUM: 6d85dee189f35e1e9a49cf8e95799a7087cfbdd5 COCOAPODS: 1.15.2 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index dc35b2b8d..626b34def 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7977,7 +7977,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 441; + CURRENT_PROJECT_VERSION = 442; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8055,7 +8055,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 441; + CURRENT_PROJECT_VERSION = 442; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index a7e1365e9..638d7e3fd 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -198,6 +198,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // Stop all jobs except for message sending and when completed suspend the database JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { Storage.suspendDatabaseAccess() + LibSession.closeNetworkConnections() } } } diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index fd24f3e64..276575511 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -216,521 +216,523 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi disappearingMessagesConfig: disappearingMessagesConfig ) } - .mapWithPrevious { [weak self, dependencies] previous, current -> [SectionModel] in - // If we don't get a `SessionThreadViewModel` then it means the thread was probably deleted - // so dismiss the screen - guard let threadViewModel: SessionThreadViewModel = current.threadViewModel else { - self?.dismissScreen(type: .popToRoot) - return [] - } - - let currentUserIsClosedGroupMember: Bool = ( - ( - threadViewModel.threadVariant == .legacyGroup || - threadViewModel.threadVariant == .group - ) && - threadViewModel.currentUserIsClosedGroupMember == true - ) - let currentUserIsClosedGroupAdmin: Bool = ( - ( - threadViewModel.threadVariant == .legacyGroup || - threadViewModel.threadVariant == .group - ) && - threadViewModel.currentUserIsClosedGroupAdmin == true - ) - let editIcon: UIImage? = UIImage(named: "icon_edit") - - return [ - SectionModel( - model: .conversationInfo, - elements: [ - SessionCell.Info( - id: .avatar, - accessory: .profile( - id: threadViewModel.id, - size: .hero, - threadVariant: threadViewModel.threadVariant, - customImageData: threadViewModel.openGroupProfilePictureData, - profile: threadViewModel.profile, - profileIcon: .none, - additionalProfile: threadViewModel.additionalProfile, - additionalProfileIcon: .none, - accessibility: nil - ), - styling: SessionCell.StyleInfo( - alignment: .centerHugging, - customPadding: SessionCell.Padding(bottom: Values.smallSpacing), - backgroundStyle: .noBackground + .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) } + + private func content(_ previous: State?, _ current: State) -> [SectionModel] { + // If we don't get a `SessionThreadViewModel` then it means the thread was probably deleted + // so dismiss the screen + guard let threadViewModel: SessionThreadViewModel = current.threadViewModel else { + self.dismissScreen(type: .popToRoot) + return [] + } + + let currentUserIsClosedGroupMember: Bool = ( + ( + threadViewModel.threadVariant == .legacyGroup || + threadViewModel.threadVariant == .group + ) && + threadViewModel.currentUserIsClosedGroupMember == true + ) + let currentUserIsClosedGroupAdmin: Bool = ( + ( + threadViewModel.threadVariant == .legacyGroup || + threadViewModel.threadVariant == .group + ) && + threadViewModel.currentUserIsClosedGroupAdmin == true + ) + let editIcon: UIImage? = UIImage(named: "icon_edit") + + return [ + SectionModel( + model: .conversationInfo, + elements: [ + SessionCell.Info( + id: .avatar, + accessory: .profile( + id: threadViewModel.id, + size: .hero, + threadVariant: threadViewModel.threadVariant, + customImageData: threadViewModel.openGroupProfilePictureData, + profile: threadViewModel.profile, + profileIcon: .none, + additionalProfile: threadViewModel.additionalProfile, + additionalProfileIcon: .none, + accessibility: nil + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + onTap: { [weak self] in self?.viewProfilePicture(threadViewModel: threadViewModel) } + ), + SessionCell.Info( + id: .nickname, + leftAccessory: (threadViewModel.threadVariant != .contact ? nil : + .icon( + editIcon?.withRenderingMode(.alwaysTemplate), + size: .fit, + customTint: .textSecondary + ) + ), + title: SessionCell.TextInfo( + threadViewModel.displayName, + font: .titleLarge, + alignment: .center, + editingPlaceholder: "CONTACT_NICKNAME_PLACEHOLDER".localized(), + interaction: (threadViewModel.threadVariant == .contact ? .editable : .none) + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + trailing: (threadViewModel.threadVariant != .contact ? + nil : + -(((editIcon?.size.width ?? 0) + (Values.smallSpacing * 2)) / 2) + ), + bottom: (threadViewModel.threadVariant != .contact ? + nil : + Values.smallSpacing + ), + interItem: 0 ), - onTap: { self?.viewProfilePicture(threadViewModel: threadViewModel) } + backgroundStyle: .noBackground ), + accessibility: Accessibility( + identifier: "Username", + label: threadViewModel.displayName + ), + onTap: { [weak self] in + self?.textChanged(self?.oldDisplayName, for: .nickname) + self?.setIsEditing(true) + } + ), + + (threadViewModel.threadVariant != .contact ? nil : SessionCell.Info( - id: .nickname, - leftAccessory: (threadViewModel.threadVariant != .contact ? nil : - .icon( - editIcon?.withRenderingMode(.alwaysTemplate), - size: .fit, - customTint: .textSecondary - ) - ), - title: SessionCell.TextInfo( - threadViewModel.displayName, - font: .titleLarge, + id: .sessionId, + subtitle: SessionCell.TextInfo( + threadViewModel.id, + font: .monoSmall, alignment: .center, - editingPlaceholder: "CONTACT_NICKNAME_PLACEHOLDER".localized(), - interaction: (threadViewModel.threadVariant == .contact ? .editable : .none) + interaction: .copy ), styling: SessionCell.StyleInfo( - alignment: .centerHugging, customPadding: SessionCell.Padding( top: Values.smallSpacing, - trailing: (threadViewModel.threadVariant != .contact ? - nil : - -(((editIcon?.size.width ?? 0) + (Values.smallSpacing * 2)) / 2) - ), - bottom: (threadViewModel.threadVariant != .contact ? - nil : - Values.smallSpacing - ), - interItem: 0 + bottom: Values.largeSpacing ), backgroundStyle: .noBackground ), accessibility: Accessibility( - identifier: "Username", - label: threadViewModel.displayName + identifier: "Session ID", + label: threadViewModel.id + ) + ) + ) + ].compactMap { $0 } + ), + SectionModel( + model: .content, + elements: [ + (threadViewModel.threadVariant == .legacyGroup || threadViewModel.threadVariant == .group ? nil : + SessionCell.Info( + id: .copyThreadId, + leftAccessory: .icon( + UIImage(named: "ic_copy")? + .withRenderingMode(.alwaysTemplate) ), - onTap: { - self?.textChanged(self?.oldDisplayName, for: .nickname) - self?.setIsEditing(true) + title: (threadViewModel.threadVariant == .community ? + "COPY_GROUP_URL".localized() : + "vc_conversation_settings_copy_session_id_button_title".localized() + ), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).copy_thread_id", + label: "Copy Session ID" + ), + onTap: { [weak self] in + switch threadViewModel.threadVariant { + case .contact, .legacyGroup, .group: + UIPasteboard.general.string = threadViewModel.threadId + + case .community: + guard + let server: String = threadViewModel.openGroupServer, + let roomToken: String = threadViewModel.openGroupRoomToken, + let publicKey: String = threadViewModel.openGroupPublicKey + else { return } + + UIPasteboard.general.string = LibSession.communityUrlFor( + server: server, + roomToken: roomToken, + publicKey: publicKey + ) + } + + self?.showToast( + text: "copied".localized(), + backgroundColor: .backgroundSecondary + ) } - ), + ) + ), - (threadViewModel.threadVariant != .contact ? nil : - SessionCell.Info( - id: .sessionId, - subtitle: SessionCell.TextInfo( - threadViewModel.id, - font: .monoSmall, - alignment: .center, - interaction: .copy - ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding( - top: Values.smallSpacing, - bottom: Values.largeSpacing - ), - backgroundStyle: .noBackground - ), - accessibility: Accessibility( - identifier: "Session ID", - label: threadViewModel.id + SessionCell.Info( + id: .allMedia, + leftAccessory: .icon( + UIImage(named: "actionsheet_camera_roll_black")? + .withRenderingMode(.alwaysTemplate) + ), + title: MediaStrings.allMedia, + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).all_media", + label: "All media" + ), + onTap: { [weak self] in + self?.transitionToScreen( + MediaGalleryViewModel.createAllMediaViewController( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + focusedAttachmentId: nil ) ) - ) - ].compactMap { $0 } - ), - SectionModel( - model: .content, - elements: [ - (threadViewModel.threadVariant == .legacyGroup || threadViewModel.threadVariant == .group ? nil : - SessionCell.Info( - id: .copyThreadId, - leftAccessory: .icon( - UIImage(named: "ic_copy")? - .withRenderingMode(.alwaysTemplate) - ), - title: (threadViewModel.threadVariant == .community ? - "COPY_GROUP_URL".localized() : - "vc_conversation_settings_copy_session_id_button_title".localized() - ), - accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).copy_thread_id", - label: "Copy Session ID" - ), - onTap: { - switch threadViewModel.threadVariant { - case .contact, .legacyGroup, .group: - UIPasteboard.general.string = threadViewModel.threadId + } + ), - case .community: - guard - let server: String = threadViewModel.openGroupServer, - let roomToken: String = threadViewModel.openGroupRoomToken, - let publicKey: String = threadViewModel.openGroupPublicKey - else { return } + SessionCell.Info( + id: .searchConversation, + leftAccessory: .icon( + UIImage(named: "conversation_settings_search")? + .withRenderingMode(.alwaysTemplate) + ), + title: "CONVERSATION_SETTINGS_SEARCH".localized(), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).search", + label: "Search" + ), + onTap: { [weak self] in + self?.didTriggerSearch() + } + ), - UIPasteboard.general.string = LibSession.communityUrlFor( - server: server, - roomToken: roomToken, - publicKey: publicKey - ) + (threadViewModel.threadVariant != .community ? nil : + SessionCell.Info( + id: .addToOpenGroup, + leftAccessory: .icon( + UIImage(named: "ic_plus_24")? + .withRenderingMode(.alwaysTemplate) + ), + title: "vc_conversation_settings_invite_button_title".localized(), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).add_to_open_group" + ), + onTap: { [weak self] in + self?.transitionToScreen( + UserSelectionVC( + with: "vc_conversation_settings_invite_button_title".localized(), + excluding: Set() + ) { [weak self] selectedUsers in + self?.addUsersToOpenGoup( + threadViewModel: threadViewModel, + selectedUsers: selectedUsers + ) } + ) + } + ) + ), - self?.showToast( - text: "copied".localized(), - backgroundColor: .backgroundSecondary + (threadViewModel.threadVariant == .community || threadViewModel.threadIsBlocked == true ? nil : + SessionCell.Info( + id: .disappearingMessages, + leftAccessory: .icon( + UIImage(systemName: "timer")? + .withRenderingMode(.alwaysTemplate) + ), + title: "DISAPPEARING_MESSAGES".localized(), + subtitle: { + guard current.disappearingMessagesConfig.isEnabled else { + return "DISAPPEARING_MESSAGES_SUBTITLE_OFF".localized() + } + guard Features.useNewDisappearingMessagesConfig else { + return String( + format: "DISAPPEARING_MESSAGES_SUBTITLE_DISAPPEAR_AFTER_LEGACY".localized(), + current.disappearingMessagesConfig.durationString ) } - ) - ), + + return String( + format: (current.disappearingMessagesConfig.type == .disappearAfterRead ? + "DISAPPEARING_MESSAGES_SUBTITLE_DISAPPEAR_AFTER_READ".localized() : + "DISAPPEARING_MESSAGES_SUBTITLE_DISAPPEAR_AFTER_SEND".localized() + ), + current.disappearingMessagesConfig.durationString + ) + }(), + accessibility: Accessibility( + identifier: "Disappearing messages", + label: "\(ThreadSettingsViewModel.self).disappearing_messages" + ), + onTap: { [weak self] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: ThreadDisappearingMessagesSettingsViewModel( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + currentUserIsClosedGroupMember: threadViewModel.currentUserIsClosedGroupMember, + currentUserIsClosedGroupAdmin: threadViewModel.currentUserIsClosedGroupAdmin, + config: current.disappearingMessagesConfig + ) + ) + ) + } + ) + ), + (!currentUserIsClosedGroupMember ? nil : SessionCell.Info( - id: .allMedia, + id: .editGroup, leftAccessory: .icon( - UIImage(named: "actionsheet_camera_roll_black")? + UIImage(named: "table_ic_group_edit")? .withRenderingMode(.alwaysTemplate) ), - title: MediaStrings.allMedia, + title: "EDIT_GROUP_ACTION".localized(), accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).all_media", - label: "All media" + identifier: "Edit group", + label: "Edit group" ), onTap: { [weak self] in self?.transitionToScreen( - MediaGalleryViewModel.createAllMediaViewController( + EditClosedGroupVC( threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - focusedAttachmentId: nil + threadVariant: threadViewModel.threadVariant ) ) } - ), + ) + ), + (!currentUserIsClosedGroupMember ? nil : SessionCell.Info( - id: .searchConversation, + id: .leaveGroup, leftAccessory: .icon( - UIImage(named: "conversation_settings_search")? + UIImage(named: "table_ic_group_leave")? .withRenderingMode(.alwaysTemplate) ), - title: "CONVERSATION_SETTINGS_SEARCH".localized(), + title: "LEAVE_GROUP_ACTION".localized(), accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).search", - label: "Search" + identifier: "Leave group", + label: "Leave group" ), - onTap: { [weak self] in - self?.didTriggerSearch() - } - ), - - (threadViewModel.threadVariant != .community ? nil : - SessionCell.Info( - id: .addToOpenGroup, - leftAccessory: .icon( - UIImage(named: "ic_plus_24")? - .withRenderingMode(.alwaysTemplate) - ), - title: "vc_conversation_settings_invite_button_title".localized(), - accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).add_to_open_group" - ), - onTap: { [weak self] in - self?.transitionToScreen( - UserSelectionVC( - with: "vc_conversation_settings_invite_button_title".localized(), - excluding: Set() - ) { [weak self] selectedUsers in - self?.addUsersToOpenGoup( - threadViewModel: threadViewModel, - selectedUsers: selectedUsers - ) - } - ) - } - ) - ), - - (threadViewModel.threadVariant == .community || threadViewModel.threadIsBlocked == true ? nil : - SessionCell.Info( - id: .disappearingMessages, - leftAccessory: .icon( - UIImage(systemName: "timer")? - .withRenderingMode(.alwaysTemplate) - ), - title: "DISAPPEARING_MESSAGES".localized(), - subtitle: { - guard current.disappearingMessagesConfig.isEnabled else { - return "DISAPPEARING_MESSAGES_SUBTITLE_OFF".localized() - } - guard Features.useNewDisappearingMessagesConfig else { - return String( - format: "DISAPPEARING_MESSAGES_SUBTITLE_DISAPPEAR_AFTER_LEGACY".localized(), - current.disappearingMessagesConfig.durationString - ) + confirmationInfo: ConfirmationModal.Info( + title: "leave_group_confirmation_alert_title".localized(), + body: .attributedText({ + if currentUserIsClosedGroupAdmin { + return NSAttributedString(string: "admin_group_leave_warning".localized()) } - return String( - format: (current.disappearingMessagesConfig.type == .disappearAfterRead ? - "DISAPPEARING_MESSAGES_SUBTITLE_DISAPPEAR_AFTER_READ".localized() : - "DISAPPEARING_MESSAGES_SUBTITLE_DISAPPEAR_AFTER_SEND".localized() - ), - current.disappearingMessagesConfig.durationString - ) - }(), - accessibility: Accessibility( - identifier: "Disappearing messages", - label: "\(ThreadSettingsViewModel.self).disappearing_messages" - ), - onTap: { [weak self] in - self?.transitionToScreen( - SessionTableViewController( - viewModel: ThreadDisappearingMessagesSettingsViewModel( - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - currentUserIsClosedGroupMember: threadViewModel.currentUserIsClosedGroupMember, - currentUserIsClosedGroupAdmin: threadViewModel.currentUserIsClosedGroupAdmin, - config: current.disappearingMessagesConfig - ) + let mutableAttributedString = NSMutableAttributedString( + string: String( + format: "leave_community_confirmation_alert_message".localized(), + threadViewModel.displayName ) ) - } - ) - ), - - (!currentUserIsClosedGroupMember ? nil : - SessionCell.Info( - id: .editGroup, - leftAccessory: .icon( - UIImage(named: "table_ic_group_edit")? - .withRenderingMode(.alwaysTemplate) - ), - title: "EDIT_GROUP_ACTION".localized(), - accessibility: Accessibility( - identifier: "Edit group", - label: "Edit group" - ), - onTap: { [weak self] in - self?.transitionToScreen( - EditClosedGroupVC( - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant - ) + mutableAttributedString.addAttribute( + .font, + value: UIFont.boldSystemFont(ofSize: Values.smallFontSize), + range: (mutableAttributedString.string as NSString).range(of: threadViewModel.displayName) + ) + return mutableAttributedString + }()), + confirmTitle: "LEAVE_BUTTON_TITLE".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text + ), + onTap: { [dependencies] in + dependencies.storage.write { db in + try SessionThread.deleteOrLeave( + db, + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + groupLeaveType: .standard, + calledFromConfigHandling: false ) } - ) - ), - - (!currentUserIsClosedGroupMember ? nil : - SessionCell.Info( - id: .leaveGroup, - leftAccessory: .icon( - UIImage(named: "table_ic_group_leave")? - .withRenderingMode(.alwaysTemplate) - ), - title: "LEAVE_GROUP_ACTION".localized(), - accessibility: Accessibility( - identifier: "Leave group", - label: "Leave group" - ), - confirmationInfo: ConfirmationModal.Info( - title: "leave_group_confirmation_alert_title".localized(), - body: .attributedText({ - if currentUserIsClosedGroupAdmin { - return NSAttributedString(string: "admin_group_leave_warning".localized()) - } - - let mutableAttributedString = NSMutableAttributedString( - string: String( - format: "leave_community_confirmation_alert_message".localized(), - threadViewModel.displayName - ) - ) - mutableAttributedString.addAttribute( - .font, - value: UIFont.boldSystemFont(ofSize: Values.smallFontSize), - range: (mutableAttributedString.string as NSString).range(of: threadViewModel.displayName) - ) - return mutableAttributedString - }()), - confirmTitle: "LEAVE_BUTTON_TITLE".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text - ), - onTap: { [weak self] in - dependencies.storage.write { db in - try SessionThread.deleteOrLeave( + } + ) + ), + + (threadViewModel.threadIsNoteToSelf ? nil : + SessionCell.Info( + id: .notificationSound, + leftAccessory: .icon( + UIImage(named: "table_ic_notification_sound")? + .withRenderingMode(.alwaysTemplate) + ), + title: "SETTINGS_ITEM_NOTIFICATION_SOUND".localized(), + rightAccessory: .dropDown( + .dynamicString { current.notificationSound.displayName } + ), + onTap: { [weak self] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: NotificationSoundViewModel(threadId: threadViewModel.threadId) + ) + ) + } + ) + ), + + (threadViewModel.threadVariant == .contact ? nil : + SessionCell.Info( + id: .notificationMentionsOnly, + leftAccessory: .icon( + UIImage(named: "NotifyMentions")? + .withRenderingMode(.alwaysTemplate) + ), + title: "vc_conversation_settings_notify_for_mentions_only_title".localized(), + subtitle: "vc_conversation_settings_notify_for_mentions_only_explanation".localized(), + rightAccessory: .toggle( + .boolValue( + threadViewModel.threadOnlyNotifyForMentions == true, + oldValue: ((previous?.threadViewModel ?? threadViewModel).threadOnlyNotifyForMentions == true) + ) + ), + isEnabled: ( + ( + threadViewModel.threadVariant != .legacyGroup && + threadViewModel.threadVariant != .group + ) || + currentUserIsClosedGroupMember + ), + accessibility: Accessibility( + identifier: "Mentions only notification setting", + label: "Mentions only" + ), + onTap: { [dependencies] in + let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true) + + dependencies.storage.writeAsync { db in + try SessionThread + .filter(id: threadViewModel.threadId) + .updateAll( db, - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - groupLeaveType: .standard, - calledFromConfigHandling: false + SessionThread.Columns.onlyNotifyForMentions + .set(to: newValue) ) - } - } - ) - ), - - (threadViewModel.threadIsNoteToSelf ? nil : - SessionCell.Info( - id: .notificationSound, - leftAccessory: .icon( - UIImage(named: "table_ic_notification_sound")? - .withRenderingMode(.alwaysTemplate) - ), - title: "SETTINGS_ITEM_NOTIFICATION_SOUND".localized(), - rightAccessory: .dropDown( - .dynamicString { current.notificationSound.displayName } - ), - onTap: { [weak self] in - self?.transitionToScreen( - SessionTableViewController( - viewModel: NotificationSoundViewModel(threadId: threadViewModel.threadId) - ) - ) } - ) - ), - - (threadViewModel.threadVariant == .contact ? nil : - SessionCell.Info( - id: .notificationMentionsOnly, - leftAccessory: .icon( - UIImage(named: "NotifyMentions")? - .withRenderingMode(.alwaysTemplate) - ), - title: "vc_conversation_settings_notify_for_mentions_only_title".localized(), - subtitle: "vc_conversation_settings_notify_for_mentions_only_explanation".localized(), - rightAccessory: .toggle( - .boolValue( - threadViewModel.threadOnlyNotifyForMentions == true, - oldValue: ((previous?.threadViewModel ?? threadViewModel).threadOnlyNotifyForMentions == true) - ) - ), - isEnabled: ( - ( - threadViewModel.threadVariant != .legacyGroup && - threadViewModel.threadVariant != .group - ) || - currentUserIsClosedGroupMember - ), - accessibility: Accessibility( - identifier: "Mentions only notification setting", - label: "Mentions only" - ), - onTap: { - let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true) + } + ) + ), + + (threadViewModel.threadIsNoteToSelf ? nil : + SessionCell.Info( + id: .notificationMute, + leftAccessory: .icon( + UIImage(named: "Mute")? + .withRenderingMode(.alwaysTemplate) + ), + title: "CONVERSATION_SETTINGS_MUTE_LABEL".localized(), + rightAccessory: .toggle( + .boolValue( + threadViewModel.threadMutedUntilTimestamp != nil, + oldValue: ((previous?.threadViewModel ?? threadViewModel).threadMutedUntilTimestamp != nil) + ) + ), + isEnabled: ( + ( + threadViewModel.threadVariant != .legacyGroup && + threadViewModel.threadVariant != .group + ) || + currentUserIsClosedGroupMember + ), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).mute", + label: "Mute notifications" + ), + onTap: { [dependencies] in + dependencies.storage.writeAsync { db in + let currentValue: TimeInterval? = try SessionThread + .filter(id: threadViewModel.threadId) + .select(.mutedUntilTimestamp) + .asRequest(of: TimeInterval.self) + .fetchOne(db) - dependencies.storage.writeAsync { db in - try SessionThread - .filter(id: threadViewModel.threadId) - .updateAll( - db, - SessionThread.Columns.onlyNotifyForMentions - .set(to: newValue) - ) - } - } - ) - ), - - (threadViewModel.threadIsNoteToSelf ? nil : - SessionCell.Info( - id: .notificationMute, - leftAccessory: .icon( - UIImage(named: "Mute")? - .withRenderingMode(.alwaysTemplate) - ), - title: "CONVERSATION_SETTINGS_MUTE_LABEL".localized(), - rightAccessory: .toggle( - .boolValue( - threadViewModel.threadMutedUntilTimestamp != nil, - oldValue: ((previous?.threadViewModel ?? threadViewModel).threadMutedUntilTimestamp != nil) - ) - ), - isEnabled: ( - ( - threadViewModel.threadVariant != .legacyGroup && - threadViewModel.threadVariant != .group - ) || - currentUserIsClosedGroupMember - ), - accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).mute", - label: "Mute notifications" - ), - onTap: { - dependencies.storage.writeAsync { db in - let currentValue: TimeInterval? = try SessionThread - .filter(id: threadViewModel.threadId) - .select(.mutedUntilTimestamp) - .asRequest(of: TimeInterval.self) - .fetchOne(db) - - try SessionThread - .filter(id: threadViewModel.threadId) - .updateAll( - db, - SessionThread.Columns.mutedUntilTimestamp.set( - to: (currentValue == nil ? - Date.distantFuture.timeIntervalSince1970 : - nil - ) + try SessionThread + .filter(id: threadViewModel.threadId) + .updateAll( + db, + SessionThread.Columns.mutedUntilTimestamp.set( + to: (currentValue == nil ? + Date.distantFuture.timeIntervalSince1970 : + nil ) ) - } + ) } - ) - ), - - (threadViewModel.threadIsNoteToSelf || threadViewModel.threadVariant != .contact ? nil : - SessionCell.Info( - id: .blockUser, - leftAccessory: .icon( - UIImage(named: "table_ic_block")? - .withRenderingMode(.alwaysTemplate) - ), - title: "CONVERSATION_SETTINGS_BLOCK_THIS_USER".localized(), - rightAccessory: .toggle( - .boolValue( - threadViewModel.threadIsBlocked == true, - oldValue: ((previous?.threadViewModel ?? threadViewModel).threadIsBlocked == true) - ) - ), - accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).block", - label: "Block" - ), - confirmationInfo: ConfirmationModal.Info( - title: { - guard threadViewModel.threadIsBlocked == true else { - return String( - format: "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT".localized(), - threadViewModel.displayName - ) - } - + } + ) + ), + + (threadViewModel.threadIsNoteToSelf || threadViewModel.threadVariant != .contact ? nil : + SessionCell.Info( + id: .blockUser, + leftAccessory: .icon( + UIImage(named: "table_ic_block")? + .withRenderingMode(.alwaysTemplate) + ), + title: "CONVERSATION_SETTINGS_BLOCK_THIS_USER".localized(), + rightAccessory: .toggle( + .boolValue( + threadViewModel.threadIsBlocked == true, + oldValue: ((previous?.threadViewModel ?? threadViewModel).threadIsBlocked == true) + ) + ), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).block", + label: "Block" + ), + confirmationInfo: ConfirmationModal.Info( + title: { + guard threadViewModel.threadIsBlocked == true else { return String( - format: "BLOCK_LIST_UNBLOCK_TITLE_FORMAT".localized(), + format: "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT".localized(), threadViewModel.displayName ) - }(), - body: (threadViewModel.threadIsBlocked == true ? .none : - .text("BLOCK_USER_BEHAVIOR_EXPLANATION".localized()) - ), - confirmTitle: (threadViewModel.threadIsBlocked == true ? - "BLOCK_LIST_UNBLOCK_BUTTON".localized() : - "BLOCK_LIST_BLOCK_BUTTON".localized() - ), - confirmAccessibility: Accessibility(identifier: "Confirm block"), - confirmStyle: .danger, - cancelStyle: .alert_text - ), - onTap: { - let isBlocked: Bool = (threadViewModel.threadIsBlocked == true) + } - self?.updateBlockedState( - from: isBlocked, - isBlocked: !isBlocked, - threadId: threadViewModel.threadId, - displayName: threadViewModel.displayName + return String( + format: "BLOCK_LIST_UNBLOCK_TITLE_FORMAT".localized(), + threadViewModel.displayName ) - } - ) + }(), + body: (threadViewModel.threadIsBlocked == true ? .none : + .text("BLOCK_USER_BEHAVIOR_EXPLANATION".localized()) + ), + confirmTitle: (threadViewModel.threadIsBlocked == true ? + "BLOCK_LIST_UNBLOCK_BUTTON".localized() : + "BLOCK_LIST_BLOCK_BUTTON".localized() + ), + confirmAccessibility: Accessibility(identifier: "Confirm block"), + confirmStyle: .danger, + cancelStyle: .alert_text + ), + onTap: { [weak self] in + let isBlocked: Bool = (threadViewModel.threadIsBlocked == true) + + self?.updateBlockedState( + from: isBlocked, + isBlocked: !isBlocked, + threadId: threadViewModel.threadId, + displayName: threadViewModel.displayName + ) + } ) - ].compactMap { $0 } - ) - ] - } + ) + ].compactMap { $0 } + ) + ] + } // MARK: - Functions diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 1fde5444d..c7805c05e 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -204,6 +204,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { if !self.hasCallOngoing() { Storage.suspendDatabaseAccess() + LibSession.closeNetworkConnections() } } } @@ -284,6 +285,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if Singleton.hasAppContext && Singleton.appContext.isInBackground { Storage.suspendDatabaseAccess() + LibSession.closeNetworkConnections() } SNLog("Background poll failed due to manual timeout") @@ -310,6 +312,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if Singleton.hasAppContext && Singleton.appContext.isInBackground { Storage.suspendDatabaseAccess() + LibSession.closeNetworkConnections() } cancelTimer.invalidate() diff --git a/Session/Shared/Types/ObservableTableSource.swift b/Session/Shared/Types/ObservableTableSource.swift index b2d4f2c22..b4d85d254 100644 --- a/Session/Shared/Types/ObservableTableSource.swift +++ b/Session/Shared/Types/ObservableTableSource.swift @@ -216,6 +216,15 @@ public extension TableObservation { .eraseToAnyPublisher() } } + + func compactMapWithPrevious(transform: @escaping (T?, T) -> R?) -> TableObservation { + return TableObservation { viewModel, dependencies in + self.generatePublisher(viewModel, dependencies) + .withPrevious() + .compactMap(transform) + .eraseToAnyPublisher() + } + } } public extension Array { diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index 14aa9536a..328c0a782 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -112,8 +112,8 @@ public extension Profile { // If we have both a `profileKey` and a `profilePicture` then the key MUST be valid if - let profileKeyData: Data = try? container.decode(Data.self, forKey: .profileEncryptionKey), - let profilePictureUrlValue: String = try? container.decode(String.self, forKey: .profilePictureUrl) + let profileKeyData: Data = try? container.decode(Data?.self, forKey: .profileEncryptionKey), + let profilePictureUrlValue: String = try? container.decode(String?.self, forKey: .profilePictureUrl) { profileKey = profileKeyData profilePictureUrl = profilePictureUrlValue @@ -122,14 +122,14 @@ public extension Profile { self = Profile( id: try container.decode(String.self, forKey: .id), name: try container.decode(String.self, forKey: .name), - lastNameUpdate: try? container.decode(TimeInterval.self, forKey: .lastNameUpdate), - nickname: try? container.decode(String.self, forKey: .nickname), + lastNameUpdate: try? container.decode(TimeInterval?.self, forKey: .lastNameUpdate), + nickname: try? container.decode(String?.self, forKey: .nickname), profilePictureUrl: profilePictureUrl, - profilePictureFileName: try? container.decode(String.self, forKey: .profilePictureFileName), + profilePictureFileName: try? container.decode(String?.self, forKey: .profilePictureFileName), profileEncryptionKey: profileKey, - lastProfilePictureUpdate: try? container.decode(TimeInterval.self, forKey: .lastProfilePictureUpdate), - blocksCommunityMessageRequests: try? container.decode(Bool.self, forKey: .blocksCommunityMessageRequests), - lastBlocksCommunityMessageRequests: try? container.decode(TimeInterval.self, forKey: .lastBlocksCommunityMessageRequests) + lastProfilePictureUpdate: try? container.decode(TimeInterval?.self, forKey: .lastProfilePictureUpdate), + blocksCommunityMessageRequests: try? container.decode(Bool?.self, forKey: .blocksCommunityMessageRequests), + lastBlocksCommunityMessageRequests: try? container.decode(TimeInterval?.self, forKey: .lastBlocksCommunityMessageRequests) ) } diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 1022cf881..2f00f73f8 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -557,7 +557,7 @@ public extension MessageViewModel { struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable, ColumnExpressible { public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { - case rowId + case rowId = "rowid" case reaction case profile } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index ecb501a67..538cd5191 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -319,6 +319,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension .map { NSNumber(value: $0) } .defaulting(to: NSNumber(value: 0)) Storage.suspendDatabaseAccess() + LibSession.closeNetworkConnections() self.contentHandler!(silentContent) } @@ -385,6 +386,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension SNLog("[NotificationServiceExtension] Show generic failure message due to error: \(error)", forceNSLog: true) DDLog.flushLog() Storage.suspendDatabaseAccess() + LibSession.closeNetworkConnections() content.title = "Session" content.body = "APN_Message".localized() diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 7344ceb79..f798d5eab 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -310,6 +310,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView receiveCompletion: { [weak self] result in DDLog.flushLog() Storage.suspendDatabaseAccess() + LibSession.closeNetworkConnections() activityIndicator.dismiss { } switch result { diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionSnodeKit/LibSession/LibSession+Networking.swift index 073396bcd..940847763 100644 --- a/SessionSnodeKit/LibSession/LibSession+Networking.swift +++ b/SessionSnodeKit/LibSession/LibSession+Networking.swift @@ -112,6 +112,12 @@ public extension LibSession { }) } + static func closeNetworkConnections() { + guard let network: UnsafeMutablePointer = networkCache.wrappedValue else { return } + + network_close_connections(network) + } + static func clearSnodeCache() { guard let network: UnsafeMutablePointer = networkCache.wrappedValue else { return } @@ -388,8 +394,16 @@ public extension LibSession { } return nodes } + + // Need to free the nodes within the path as we are the owner + cPaths.forEach { cPath in + cPath.nodes.deallocate() + } } + // Need to free the cPathsPtr as we are the owner + cPathsPtr?.deallocate() + lastPaths.mutate { lastPaths in lastPaths = paths diff --git a/SessionUtilitiesKit/LibSession/Utilities/TypeConversion+Utilities.swift b/SessionUtilitiesKit/LibSession/Utilities/TypeConversion+Utilities.swift index 10fb9d1fd..4ec0abd84 100644 --- a/SessionUtilitiesKit/LibSession/Utilities/TypeConversion+Utilities.swift +++ b/SessionUtilitiesKit/LibSession/Utilities/TypeConversion+Utilities.swift @@ -139,8 +139,9 @@ public extension Array where Element == String { count: Int? ) { guard - let pointee: UnsafeMutablePointer = pointer?.pointee, - let count: Int = count + let count: Int = count, + count > 0, + let pointee: UnsafeMutablePointer = pointer?.pointee else { return nil } self = (0..