From 42721db399dba2653ecc0b0363639e23c93cd54e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 11 Oct 2024 16:21:23 +1100 Subject: [PATCH] Added additional developer settings and fixed some bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added a setting to show the string keys instead of localised values • Added settings to test the updated groups delete all messages settings • Updated the onboarding UI to indicate whether it's currently pointing at Testnet • Tweaked the 'isRTL' dependency setting to be clearer that it's not an injected value • Fixed a bug with dependency name collision between the different storage types • Fixed a bug with attachment file path creation --- .../VoiceMessageRecordingView.swift | 6 +- .../Message Cells/VisibleMessageCell.swift | 15 +- .../Settings/ThreadSettingsViewModel.swift | 101 +++++++++++++- Session/Home/HomeVC.swift | 22 +-- .../SendMediaNavigationController.swift | 2 +- Session/Meta/AppDelegate.swift | 6 +- Session/Onboarding/DisplayNameScreen.swift | 2 +- Session/Onboarding/LandingScreen.swift | 2 +- Session/Onboarding/LoadAccountScreen.swift | 2 +- Session/Onboarding/LoadingScreen.swift | 9 +- Session/Onboarding/PNModeScreen.swift | 2 +- .../Settings/AppearanceViewController.swift | 2 +- .../Settings/DeveloperSettingsViewModel.swift | 130 +++++++++++++++++- Session/Settings/SettingsViewModel.swift | 10 +- Session/Shared/BaseVC.swift | 10 -- Session/Shared/FullConversationCell.swift | 2 +- .../Shared/SessionHostingViewController.swift | 60 +++++++- .../Database/Models/Attachment.swift | 10 +- .../NotificationServiceExtension.swift | 11 +- .../ShareNavController.swift | 2 +- .../LibSession/LibSession+Networking.swift | 19 ++- .../Components/ProfilePictureView.swift | 8 +- .../Dependency Injection/Dependencies.swift | 105 +++++++++----- SessionUtilitiesKit/General/Feature.swift | 16 +++ .../General/Localization.swift | 4 + .../General/UIEdgeInsets.swift | 4 +- .../Media/UTType+Utilities.swift | 2 +- .../Utilities/UIViewController+OWS.swift | 4 +- 28 files changed, 461 insertions(+), 107 deletions(-) diff --git a/Session/Conversations/Input View/VoiceMessageRecordingView.swift b/Session/Conversations/Input View/VoiceMessageRecordingView.swift index 9d782e387..25a1985fa 100644 --- a/Session/Conversations/Input View/VoiceMessageRecordingView.swift +++ b/Session/Conversations/Input View/VoiceMessageRecordingView.swift @@ -62,7 +62,7 @@ final class VoiceMessageRecordingView: UIView { private lazy var chevronImageView: UIImageView = { let result: UIImageView = UIImageView( - image: (Dependencies.isRTL ? + image: (Dependencies.unsafeNonInjected.isRTL ? UIImage(named: "small_chevron_left")?.withHorizontallyFlippedOrientation() : UIImage(named: "small_chevron_left") )? @@ -276,9 +276,9 @@ final class VoiceMessageRecordingView: UIView { // MARK: - Interaction func handleLongPressMoved(to location: CGPoint) { - if ((!Dependencies.isRTL && location.x < bounds.center.x) || (Dependencies.isRTL && location.x > bounds.center.x)) { + if ((!Dependencies.unsafeNonInjected.isRTL && location.x < bounds.center.x) || (Dependencies.unsafeNonInjected.isRTL && location.x > bounds.center.x)) { let translationX = location.x - bounds.center.x - let sign: CGFloat = (Dependencies.isRTL ? 1 : -1) + let sign: CGFloat = (Dependencies.unsafeNonInjected.isRTL ? 1 : -1) let chevronDamping: CGFloat = 4 let labelDamping: CGFloat = 3 let chevronX = (chevronDamping * (sqrt(abs(translationX)) / sqrt(chevronDamping))) * sign diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index edc4be933..501755196 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -364,7 +364,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // Flip horizontally for RTL languages replyIconImageView.transform = CGAffineTransform.identity .scaledBy( - x: (Dependencies.isRTL ? -1 : 1), + x: (Dependencies.unsafeNonInjected.isRTL ? -1 : 1), y: 1 ) @@ -802,7 +802,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let v = panGestureRecognizer.velocity(in: self) // Only allow swipes to the left; allowing swipes to the right gets in the way of // the default iOS swipe to go back gesture - guard (Dependencies.isRTL && v.x > 0) || (!Dependencies.isRTL && v.x < 0) else { return false } + guard + (Dependencies.unsafeNonInjected.isRTL && v.x > 0) || + (!Dependencies.unsafeNonInjected.isRTL && v.x < 0) + else { return false } return abs(v.x) > abs(v.y) // It has to be more horizontal than vertical } @@ -938,8 +941,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { .translation(in: self) .x .clamp( - (Dependencies.isRTL ? 0 : -CGFloat.greatestFiniteMagnitude), - (Dependencies.isRTL ? CGFloat.greatestFiniteMagnitude : 0) + (Dependencies.unsafeNonInjected.isRTL ? 0 : -CGFloat.greatestFiniteMagnitude), + (Dependencies.unsafeNonInjected.isRTL ? CGFloat.greatestFiniteMagnitude : 0) ) switch gestureRecognizer.state { @@ -948,7 +951,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { case .changed: // The idea here is to asymptotically approach a maximum drag distance let damping: CGFloat = 20 - let sign: CGFloat = (Dependencies.isRTL ? 1 : -1) + let sign: CGFloat = (Dependencies.unsafeNonInjected.isRTL ? 1 : -1) let x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign viewsToMoveForReply.forEach { $0.transform = CGAffineTransform(translationX: x, y: 0) } @@ -1203,7 +1206,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // we only highlight those cases) normalizedBody .ranges( - of: (Dependencies.isRTL ? + of: (Dependencies.unsafeNonInjected.isRTL ? "(\(part.lowercased()))(^|[^a-zA-Z0-9])" : "(^|[^a-zA-Z0-9])(\(part.lowercased()))" ), diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 85403511d..9326ba94c 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -60,6 +60,15 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob public enum Section: SessionTableSection { case conversationInfo case content + case adminActions + case destructiveActions + + public var style: SessionTableSectionStyle { + switch self { + case .destructiveActions: return .padding + default: return .none + } + } } public enum TableItem: Differentiable { @@ -81,6 +90,9 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob case notificationMentionsOnly case notificationMute case blockUser + + case debugDeleteBeforeNow + case debugDeleteAttachmentsBeforeNow } // MARK: - Content @@ -693,10 +705,71 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob ) ].compactMap { $0 } ) + let adminActionsSection: SectionModel? = nil + let destructiveActionsSection: SectionModel? + + if dependencies[feature: .updatedGroupsDeleteBeforeNow] || dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] { + destructiveActionsSection = SectionModel( + model: .destructiveActions, + elements: [ + // FIXME: [GROUPS REBUILD] Need to build this properly in a future release + (!dependencies[feature: .updatedGroupsDeleteBeforeNow] || threadViewModel.threadVariant != .group ? nil : + SessionCell.Info( + id: .debugDeleteBeforeNow, + leadingAccessory: .icon( + UIImage(named: "ic_bin")? + .withRenderingMode(.alwaysTemplate), + customTint: .danger + ), + title: "[DEBUG] Delete all messages before now", // stringlint:disable + styling: SessionCell.StyleInfo( + tintColor: .danger + ), + confirmationInfo: ConfirmationModal.Info( + title: "delete".localized(), + body: .text("Are you sure you want to delete all messages sent before now for all group members?"), // stringlint:disable + confirmTitle: "delete".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text + ), + onTap: { [weak self] in self?.deleteAllMessagesBeforeNow() } + ) + ), + // FIXME: [GROUPS REBUILD] Need to build this properly in a future release + (!dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] || threadViewModel.threadVariant != .group ? nil : + SessionCell.Info( + id: .debugDeleteAttachmentsBeforeNow, + leadingAccessory: .icon( + UIImage(named: "ic_bin")? + .withRenderingMode(.alwaysTemplate), + customTint: .danger + ), + title: "[DEBUG] Delete all arrachments before now", // stringlint:disable + styling: SessionCell.StyleInfo( + tintColor: .danger + ), + confirmationInfo: ConfirmationModal.Info( + title: "delete".localized(), + body: .text("Are you sure you want to delete all attachments (and their associated messages) sent before now for all group members?"), // stringlint:disable + confirmTitle: "delete".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text + ), + onTap: { [weak self] in self?.deleteAllAttachmentsBeforeNow() } + ) + ) + ].compactMap { $0 } + ) + } + else { + destructiveActionsSection = nil + } return [ conversationInfoSection, - standardActionsSection + standardActionsSection, + adminActionsSection, + destructiveActionsSection ].compactMap { $0 } } @@ -1341,4 +1414,30 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob ) } } + + private func deleteAllMessagesBeforeNow() { + guard threadVariant == .group else { return } + + dependencies[singleton: .storage].writeAsync { [threadId, dependencies] db in + try LibSession.deleteMessagesBefore( + db, + groupSessionId: SessionId(.group, hex: threadId), + timestamp: dependencies.dateNow.timeIntervalSince1970, + using: dependencies + ) + } + } + + private func deleteAllAttachmentsBeforeNow() { + guard threadVariant == .group else { return } + + dependencies[singleton: .storage].writeAsync { [threadId, dependencies] db in + try LibSession.deleteAttachmentsBefore( + db, + groupSessionId: SessionId(.group, hex: threadId), + timestamp: dependencies.dateNow.timeIntervalSince1970, + using: dependencies + ) + } + } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 59b6cbdec..e7812f8da 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -579,10 +579,13 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi threadVariant: .contact, displayPictureFilename: nil, profile: userProfile, - profileIcon: (viewModel.dependencies[feature: .serviceNetwork] == .testnet ? - .letter("T") : // stringlint:disable - .none - ), + profileIcon: { + switch (viewModel.dependencies[feature: .serviceNetwork], viewModel.dependencies[feature: .forceOffline]) { + case (.testnet, false): return .letter("T", false) // stringlint:disable + case (.testnet, true): return .letter("T", true) // stringlint:disable + default: return .none + } + }(), additionalProfile: nil, using: viewModel.dependencies ) @@ -612,10 +615,13 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi threadVariant: .contact, displayPictureFilename: nil, profile: userProfile, - profileIcon: (value == .testnet ? - .letter("T") : // stringlint:disable - .none - ), + profileIcon: { + switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) { + case (.testnet, false): return .letter("T", false) // stringlint:disable + case (.testnet, true): return .letter("T", true) // stringlint:disable + default: return .none + } + }(), additionalProfile: nil, using: dependencies ) diff --git a/Session/Media Viewing & Editing/SendMediaNavigationController.swift b/Session/Media Viewing & Editing/SendMediaNavigationController.swift index 594cc1fa7..8820431f6 100644 --- a/Session/Media Viewing & Editing/SendMediaNavigationController.swift +++ b/Session/Media Viewing & Editing/SendMediaNavigationController.swift @@ -647,7 +647,7 @@ private class DoneButton: UIView { private lazy var chevron: UIView = { let image: UIImage = { - guard Dependencies.isRTL else { return #imageLiteral(resourceName: "small_chevron_right") } + guard Dependencies.unsafeNonInjected.isRTL else { return #imageLiteral(resourceName: "small_chevron_right") } return #imageLiteral(resourceName: "small_chevron_left") }() diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 25490e16a..eb6abb036 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -642,7 +642,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let rootViewControllerSetupComplete: (UIViewController) -> () = { [weak self, dependencies] rootViewController in /// `MainAppContext.determineDeviceRTL` uses UIKit to retrime `isRTL` so must be run on the main thread to prevent /// lag/crashes on background threads - Dependencies.setIsRTLRetriever(requiresMainThread: true) { MainAppContext.determineDeviceRTL() } + dependencies.setIsRTLRetriever(requiresMainThread: true) { MainAppContext.determineDeviceRTL() } /// Setup the `TopBannerController` let presentedViewController: UIViewController? = self?.window?.rootViewController?.presentedViewController @@ -706,7 +706,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let viewController = SessionHostingViewController(rootView: LandingScreen(using: dependencies) { [weak self] in self?.handleActivation() }) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: dependencies) longRunningStartupTimoutCancellable.cancel() rootViewControllerSetupComplete(viewController) } @@ -714,7 +714,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD case .missingName: DispatchQueue.main.async { [dependencies] in let viewController = SessionHostingViewController(rootView: DisplayNameScreen(using: dependencies)) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: dependencies) longRunningStartupTimoutCancellable.cancel() rootViewControllerSetupComplete(viewController) diff --git a/Session/Onboarding/DisplayNameScreen.swift b/Session/Onboarding/DisplayNameScreen.swift index 7bebf5e97..e6252eb88 100644 --- a/Session/Onboarding/DisplayNameScreen.swift +++ b/Session/Onboarding/DisplayNameScreen.swift @@ -142,7 +142,7 @@ struct DisplayNameScreen: View { let viewController: SessionHostingViewController = SessionHostingViewController( rootView: PNModeScreen(using: dependencies) ) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } } diff --git a/Session/Onboarding/LandingScreen.swift b/Session/Onboarding/LandingScreen.swift index 8ce510f00..88d5ab96f 100644 --- a/Session/Onboarding/LandingScreen.swift +++ b/Session/Onboarding/LandingScreen.swift @@ -163,7 +163,7 @@ struct LandingScreen: View { let viewController: SessionHostingViewController = SessionHostingViewController( rootView: DisplayNameScreen(using: viewModel.dependencies) ) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: viewModel.dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } } diff --git a/Session/Onboarding/LoadAccountScreen.swift b/Session/Onboarding/LoadAccountScreen.swift index b428fc3a5..41eefd225 100644 --- a/Session/Onboarding/LoadAccountScreen.swift +++ b/Session/Onboarding/LoadAccountScreen.swift @@ -75,7 +75,7 @@ struct LoadAccountScreen: View { let viewController: SessionHostingViewController = SessionHostingViewController( rootView: PNModeScreen(using: dependencies) ) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index 064a32376..ce238b520 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -127,11 +127,12 @@ struct LoadingScreen: View { let viewController: SessionHostingViewController = SessionHostingViewController( rootView: DisplayNameScreen(using: viewModel.dependencies) ) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: viewModel.dependencies) if let navigationController = self.host.controller?.navigationController { - let index = navigationController.viewControllers.count - 1 - navigationController.pushViewController(viewController, animated: true) - navigationController.viewControllers.remove(at: index) + let updatedViewControllers: [UIViewController] = navigationController.viewControllers + .filter { !$0.isKind(of: SessionHostingViewController.self) } + .appending(viewController) + navigationController.setViewControllers(updatedViewControllers, animated: true) } return } diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index 4e630b88f..969ad57ea 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -145,7 +145,7 @@ struct PNModeScreen: View { let viewController: SessionHostingViewController = SessionHostingViewController( rootView: LoadingScreen(using: dependencies) ) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } diff --git a/Session/Settings/AppearanceViewController.swift b/Session/Settings/AppearanceViewController.swift index 9e40ec540..a980cdda4 100644 --- a/Session/Settings/AppearanceViewController.swift +++ b/Session/Settings/AppearanceViewController.swift @@ -108,7 +108,7 @@ final class AppearanceViewController: BaseVC { trailing: Values.largeSpacing ) - if Dependencies.isRTL { + if Dependencies.unsafeNonInjected.isRTL { result.transform = CGAffineTransform.identity.scaledBy(x: -1, y: 1) } diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index e6802dc57..38d51f263 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -30,6 +30,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum Section: SessionTableSection { case developerMode + case general case logging case network case disappearingMessages @@ -39,6 +40,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, var title: String? { switch self { case .developerMode: return nil + case .general: return "General" case .logging: return "Logging" case .network: return "Network" case .disappearingMessages: return "Disappearing Messages" @@ -58,11 +60,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public enum TableItem: Hashable, Differentiable, CaseIterable { case developerMode + case showStringKeys + case defaultLogLevel case advancedLogging case loggingCategory(String) case serviceNetwork + case forceOffline case resetSnodeCache case updatedDisappearingMessages @@ -76,6 +81,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case updatedGroupsAllowDescriptionEditing case updatedGroupsAllowPromotions case updatedGroupsAllowInviteById + case updatedGroupsDeleteBeforeNow + case updatedGroupsDeleteAttachmentsBeforeNow case exportDatabase @@ -86,11 +93,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, public var differenceIdentifier: String { switch self { case .developerMode: return "developerMode" + case .showStringKeys: return "showStringKeys" + case .defaultLogLevel: return "defaultLogLevel" case .advancedLogging: return "advancedLogging" case .loggingCategory(let categoryIdentifier): return "loggingCategory-\(categoryIdentifier)" case .serviceNetwork: return "serviceNetwork" + case .forceOffline: return "forceOffline" case .resetSnodeCache: return "resetSnodeCache" case .updatedDisappearingMessages: return "updatedDisappearingMessages" @@ -104,6 +114,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .updatedGroupsAllowDescriptionEditing: return "updatedGroupsAllowDescriptionEditing" case .updatedGroupsAllowPromotions: return "updatedGroupsAllowPromotions" case .updatedGroupsAllowInviteById: return "updatedGroupsAllowInviteById" + case .updatedGroupsDeleteBeforeNow: return "updatedGroupsDeleteBeforeNow" + case .updatedGroupsDeleteAttachmentsBeforeNow: return "updatedGroupsDeleteAttachmentsBeforeNow" case .exportDatabase: return "exportDatabase" } @@ -117,11 +129,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, var result: [TableItem] = [] switch TableItem.developerMode { case .developerMode: result.append(.developerMode); fallthrough + case .showStringKeys: result.append(.showStringKeys); fallthrough + case .defaultLogLevel: result.append(.defaultLogLevel); fallthrough case .advancedLogging: result.append(.advancedLogging); fallthrough case .loggingCategory: result.append(.loggingCategory("")); fallthrough case .serviceNetwork: result.append(.serviceNetwork); fallthrough + case .forceOffline: result.append(.forceOffline); fallthrough case .resetSnodeCache: result.append(.resetSnodeCache); fallthrough case .updatedDisappearingMessages: result.append(.updatedDisappearingMessages); fallthrough @@ -136,6 +151,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .updatedGroupsAllowDescriptionEditing: result.append(.updatedGroupsAllowDescriptionEditing); fallthrough case .updatedGroupsAllowPromotions: result.append(.updatedGroupsAllowPromotions); fallthrough case .updatedGroupsAllowInviteById: result.append(.updatedGroupsAllowInviteById); fallthrough + case .updatedGroupsDeleteBeforeNow: result.append(.updatedGroupsDeleteBeforeNow); fallthrough + case .updatedGroupsDeleteAttachmentsBeforeNow: result.append(.updatedGroupsDeleteAttachmentsBeforeNow); fallthrough case .exportDatabase: result.append(.exportDatabase) } @@ -149,11 +166,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, private struct State: Equatable { let developerMode: Bool + let showStringKeys: Bool + let defaultLogLevel: Log.Level let advancedLogging: Bool let loggingCategories: [Log.Category: Log.Level] let serviceNetwork: ServiceNetwork + let forceOffline: Bool let debugDisappearingMessageDurations: Bool let updatedDisappearingMessages: Bool @@ -166,6 +186,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let updatedGroupsAllowDescriptionEditing: Bool let updatedGroupsAllowPromotions: Bool let updatedGroupsAllowInviteById: Bool + let updatedGroupsDeleteBeforeNow: Bool + let updatedGroupsDeleteAttachmentsBeforeNow: Bool } let title: String = "Developer Settings" @@ -174,12 +196,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, .refreshableData(self) { [weak self, dependencies] () -> State in State( developerMode: dependencies[singleton: .storage, key: .developerModeEnabled], + showStringKeys: dependencies[feature: .showStringKeys], defaultLogLevel: dependencies[feature: .logLevel(cat: .default)], advancedLogging: (self?.showAdvancedLogging == true), loggingCategories: dependencies[feature: .allLogLevels].currentValues(using: dependencies), serviceNetwork: dependencies[feature: .serviceNetwork], + forceOffline: dependencies[feature: .forceOffline], debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations], updatedDisappearingMessages: dependencies[feature: .updatedDisappearingMessages], @@ -191,7 +215,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], - updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById] + updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById], + updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], + updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] ) } .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) } @@ -223,6 +249,32 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) ] ), + SectionModel( + model: .general, + elements: [ + SessionCell.Info( + id: .showStringKeys, + title: "Show String Keys", + subtitle: """ + Controls whether localised strings should render using their keys rather than the localised value (strings will be rendered as "[{key}]") + + Notes: + • This change will only apply to newly created screens (eg. the Settings screen will need to be closed and reopened before it gets updated + • The "Home" screen won't update as it never gets recreated + """, + trailingAccessory: .toggle( + current.showStringKeys, + oldValue: previous?.showStringKeys + ), + onTap: { [weak self] in + self?.updateFlag( + for: .showStringKeys, + to: !current.showStringKeys + ) + } + ) + ] + ), SectionModel( model: .logging, elements: [ @@ -304,7 +356,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, The environment used for sending requests and storing messages. Warning: - Changing this setting will result in all conversation and snode data being cleared and any pending network requests being cancelled. + Changing between some of these options can result in all conversation and snode data being cleared and any pending network requests being cancelled. """, trailingAccessory: .dropDown { current.serviceNetwork.title }, onTap: { [weak self, dependencies] in @@ -323,6 +375,18 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) } ), + SessionCell.Info( + id: .forceOffline, + title: "Force Offline", + subtitle: """ + Shut down the current network and cause all future network requests to fail after a 1 second delay with a 'serviceUnavailable' error. + """, + trailingAccessory: .toggle( + current.forceOffline, + oldValue: previous?.forceOffline + ), + onTap: { [weak self] in self?.updateForceOffline(current: current.forceOffline) } + ), SessionCell.Info( id: .resetSnodeCache, title: "Reset Service Node Cache", @@ -513,7 +577,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, id: .updatedGroupsAllowInviteById, title: "Allow Invite by ID", subtitle: """ - Controls whether the UI allows group admins to invlide other group members directly by their Account ID. + Controls whether the UI allows group admins to invite other group members directly by their Account ID. Note: In a future release we will offer this functionality but it's not included in the initial release. """, @@ -527,6 +591,44 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, to: !current.updatedGroupsAllowInviteById ) } + ), + SessionCell.Info( + id: .updatedGroupsDeleteBeforeNow, + title: "Show button to delete messages before now", + subtitle: """ + Controls whether the UI allows group admins to delete all messages in the group that were sent before the button was pressed. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + current.updatedGroupsDeleteBeforeNow, + oldValue: previous?.updatedGroupsDeleteBeforeNow + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsDeleteBeforeNow, + to: !current.updatedGroupsDeleteBeforeNow + ) + } + ), + SessionCell.Info( + id: .updatedGroupsDeleteAttachmentsBeforeNow, + title: "Show button to delete attachments before now", + subtitle: """ + Controls whether the UI allows group admins to delete all attachments (and their associated messages) in the group that were sent before the button was pressed. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + current.updatedGroupsDeleteAttachmentsBeforeNow, + oldValue: previous?.updatedGroupsDeleteAttachmentsBeforeNow + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsDeleteAttachmentsBeforeNow, + to: !current.updatedGroupsDeleteAttachmentsBeforeNow + ) + } ) ] ), @@ -559,6 +661,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, TableItem.allCases.forEach { item in switch item { case .developerMode: break // Not a feature + case .showStringKeys: updateFlag(for: .showStringKeys, to: nil) + case .resetSnodeCache: break // Not a feature case .exportDatabase: break // Not a feature case .advancedLogging: break // Not a feature @@ -567,6 +671,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .loggingCategory: resetLoggingCategories() case .serviceNetwork: updateServiceNetwork(to: nil) + case .forceOffline: updateFlag(for: .forceOffline, to: nil) case .debugDisappearingMessageDurations: updateFlag(for: .debugDisappearingMessageDurations, to: nil) @@ -582,6 +687,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updateFlag(for: .updatedGroupsAllowDescriptionEditing, to: nil) case .updatedGroupsAllowPromotions: updateFlag(for: .updatedGroupsAllowPromotions, to: nil) case .updatedGroupsAllowInviteById: updateFlag(for: .updatedGroupsAllowInviteById, to: nil) + case .updatedGroupsDeleteBeforeNow: updateFlag(for: .updatedGroupsDeleteBeforeNow, to: nil) + case .updatedGroupsDeleteAttachmentsBeforeNow: updateFlag(for: .updatedGroupsDeleteAttachmentsBeforeNow, to: nil) } } @@ -738,6 +845,17 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, forceRefresh(type: .databaseQuery) } + private func updateForceOffline(current: Bool) { + updateFlag(for: .forceOffline, to: !current) + + // Reset the network cache + dependencies.mutate(cache: .libSessionNetwork) { + $0.setPaths(paths: []) + $0.setNetworkStatus(status: current ? .unknown : .disconnected) + } + dependencies.remove(cache: .libSessionNetwork) + } + private func resetServiceNodeCache() { self.transitionToScreen( ConfirmationModal( @@ -894,5 +1012,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, // MARK: - Listable Conformance -extension ServiceNetwork: Listable {} +extension ServiceNetwork: @retroactive ContentIdentifiable {} +extension ServiceNetwork: @retroactive ContentEquatable {} +extension ServiceNetwork: @retroactive Listable {} +extension Log.Level: @retroactive ContentIdentifiable {} +extension Log.Level: @retroactive ContentEquatable {} extension Log.Level: Listable {} diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 2bae691a2..15c769e8a 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -143,9 +143,13 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl id: state.profile.id, size: .hero, profile: state.profile, - profileIcon: (dependencies[feature: .serviceNetwork] == .mainnet ? .none : - .letter("T") // stringlint:disable - ) + profileIcon: { + switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) { + case (.testnet, false): return .letter("T", false) // stringlint:disable + case (.testnet, true): return .letter("T", true) // stringlint:disable + default: return .none + } + }() ), styling: SessionCell.StyleInfo( alignment: .centerHugging, diff --git a/Session/Shared/BaseVC.swift b/Session/Shared/BaseVC.swift index 4e2dc7ec1..c6bff73df 100644 --- a/Session/Shared/BaseVC.swift +++ b/Session/Shared/BaseVC.swift @@ -69,14 +69,4 @@ public class BaseVC: UIViewController { navigationItem.titleView = headingImageView } - - internal func setUpNavBarSessionIcon() { - let logoImageView = UIImageView() - logoImageView.image = #imageLiteral(resourceName: "SessionGreen32") - logoImageView.contentMode = .scaleAspectFit - logoImageView.set(.width, to: 32) - logoImageView.set(.height, to: 32) - - navigationItem.titleView = logoImageView - } } diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index f50399009..c68265f15 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -741,7 +741,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC // with the term so we use the regex below to ensure we only highlight those cases) normalizedSnippet .ranges( - of: (Dependencies.isRTL ? + of: (Dependencies.unsafeNonInjected.isRTL ? "(\(part.lowercased()))(^|[^a-zA-Z0-9])" : // stringlint:disable "(^|[^a-zA-Z0-9])(\(part.lowercased()))" // stringlint:disable ), diff --git a/Session/Shared/SessionHostingViewController.swift b/Session/Shared/SessionHostingViewController.swift index 261068c93..a128f7ba6 100644 --- a/Session/Shared/SessionHostingViewController.swift +++ b/Session/Shared/SessionHostingViewController.swift @@ -111,14 +111,70 @@ public class SessionHostingViewController: UIHostingController String? { - // Store the file in a subdirectory whose name is the uniqueId of this attachment, - // to avoid collisions between multiple attachments with the same name - let attachmentFolder: String = Attachment.attachmentsFolder(using: dependencies).appending("/\(id)") - if let sourceFilename: String = sourceFilename, !sourceFilename.isEmpty { // Ensure that the filename is a valid filesystem name, // replacing invalid characters with an underscore. @@ -652,6 +648,10 @@ extension Attachment { targetFileExtension = targetFileExtension.lowercased() if !targetFileExtension.isEmpty { + // Store the file in a subdirectory whose name is the uniqueId of this attachment, + // to avoid collisions between multiple attachments with the same name + let attachmentFolder: String = Attachment.attachmentsFolder(using: dependencies).appending("/\(id)") + guard case .success = Result(try FileSystem.ensureDirectoryExists(at: attachmentFolder, using: dependencies)) else { return nil } @@ -665,7 +665,7 @@ extension Attachment { UTType.fileExtensionDefault ).lowercased() - return attachmentFolder.appending("/\(id).\(targetFileExtension)") + return Attachment.attachmentsFolder(using: dependencies).appending("/\(id).\(targetFileExtension)") } public static func localRelativeFilePath(from originalFilePath: String?, using dependencies: Dependencies) -> String? { diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 717953731..b10167e37 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -34,6 +34,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension self.contentHandler = contentHandler self.request = request + /// Create a new `Dependencies` instance each time so we don't need to worry about state from previous + /// notifications causing issues with new notifications + self.dependencies = Dependencies.createEmpty() + // It's technically possible for 'completeSilently' to be called twice due to the NSE timeout so self.hasCompleted.mutate { $0 = false } @@ -51,15 +55,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension /// Create the context if we don't have it (needed before _any_ interaction with the database) if !dependencies[singleton: .appContext].isValid { dependencies.set(singleton: .appContext, to: NotificationServiceExtensionContext(using: dependencies)) - Dependencies.setIsRTLRetriever(requiresMainThread: false) { + dependencies.setIsRTLRetriever(requiresMainThread: false) { NotificationServiceExtensionContext.determineDeviceRTL() } } - /// Perform main setup (create a new `Dependencies` instance each time so we don't need to worry about state from previous - /// notifications causing issues with new notifications - self.dependencies = Dependencies.createEmpty() - + /// Actually perform the setup DispatchQueue.main.sync { self.performSetup { [weak self] in self?.handleNotification(notificationContent, isPerformingResetup: false) diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index f9ce94a30..84d13582c 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -37,7 +37,7 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { /// to override it results in the share context crashing so ensure it doesn't exist first) if !dependencies[singleton: .appContext].isValid { dependencies.set(singleton: .appContext, to: ShareAppExtensionContext(rootViewController: self, using: dependencies)) - Dependencies.setIsRTLRetriever(requiresMainThread: false) { ShareAppExtensionContext.determineDeviceRTL() } + dependencies.setIsRTLRetriever(requiresMainThread: false) { ShareAppExtensionContext.determineDeviceRTL() } } guard !SNUtilitiesKit.isRunningTests else { return } diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionSnodeKit/LibSession/LibSession+Networking.swift index 9c9019e57..17c3d53ad 100644 --- a/SessionSnodeKit/LibSession/LibSession+Networking.swift +++ b/SessionSnodeKit/LibSession/LibSession+Networking.swift @@ -664,6 +664,14 @@ public extension LibSession { // Create the network object getOrCreateNetwork().sinkUntilComplete() + + // If the app has been set to 'forceOffline' then we need to explicitly set the network status + // to disconnected (because it'll never be set otherwise) + if dependencies[feature: .forceOffline] { + DispatchQueue.global(qos: .default).async { [dependencies] in + dependencies.mutate(cache: .libSessionNetwork) { $0.setNetworkStatus(status: .disconnected) } + } + } } deinit { @@ -713,13 +721,18 @@ public extension LibSession { return Fail(error: NetworkError.suspended).eraseToAnyPublisher() } - switch network { - case .some(let existingNetwork): + switch (network, dependencies[feature: .forceOffline]) { + case (_, true): + return Fail(error: NetworkError.serviceUnavailable) + .delay(for: .seconds(1), scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + + case (.some(let existingNetwork), _): return Just(existingNetwork) .setFailureType(to: Error.self) .eraseToAnyPublisher() - case .none: + case (.none, _): let useTestnet: Bool = (dependencies[feature: .serviceNetwork] == .testnet) let isMainApp: Bool = dependencies[singleton: .appContext].isMainApp var error: [CChar] = [CChar](repeating: 0, count: 256) diff --git a/SessionUIKit/Components/ProfilePictureView.swift b/SessionUIKit/Components/ProfilePictureView.swift index cdf196b6f..d83f427d8 100644 --- a/SessionUIKit/Components/ProfilePictureView.swift +++ b/SessionUIKit/Components/ProfilePictureView.swift @@ -76,7 +76,7 @@ public final class ProfilePictureView: UIView { case none case crown case rightPlus - case letter(Character) + case letter(Character, Bool) func iconVerticalInset(for size: Size) -> CGFloat { switch (self, size) { @@ -438,9 +438,9 @@ public final class ProfilePictureView: UIView { imageView.isHidden = false label.isHidden = true - case .letter(let character): - label.themeTextColor = .backgroundPrimary - backgroundView.themeBackgroundColor = .textPrimary + case .letter(let character, let dangerMode): + label.themeTextColor = (dangerMode ? .textPrimary : .backgroundPrimary) + backgroundView.themeBackgroundColor = (dangerMode ? .danger : .textPrimary) label.isHidden = false label.text = "\(character)" } diff --git a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift index 5d6644850..b314bec0f 100644 --- a/SessionUtilitiesKit/Dependency Injection/Dependencies.swift +++ b/SessionUtilitiesKit/Dependency Injection/Dependencies.swift @@ -8,7 +8,7 @@ import Combine public class Dependencies { static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "session.dependencies.codingOptions")! - private static var _isRTLRetriever: Atomic<(Bool, () -> Bool)> = Atomic((false, { false })) + private static var _lastCreatedInstance: Atomic = Atomic(nil) private let featureChangeSubject: PassthroughSubject<(String, String?, Any?), Never> = PassthroughSubject() private var storage: Atomic = Atomic(DependencyStorage()) @@ -21,8 +21,12 @@ public class Dependencies { // MARK: - Global Values, Timing and Async Handling - public static var isRTL: Bool { - let (requiresMainThread, retriever): (Bool, () -> Bool) = _isRTLRetriever.wrappedValue + /// We should avoid using this value wherever possible because it's not properly injected (which means unit tests won't work correctly + /// for anything accessed via this value) + public static var unsafeNonInjected: Dependencies { _lastCreatedInstance.wrappedValue ?? Dependencies() } + + public var isRTL: Bool { + let (requiresMainThread, retriever): (Bool, () -> Bool) = storage.wrappedValue.isRTLRetriever /// Determining `isRTL` might require running on the main thread (it may need to accesses UIKit), if it requires the main thread but /// we are on a different thread then just default to `false` to prevent the background thread from potentially lagging and/or crashing @@ -37,7 +41,9 @@ public class Dependencies { // MARK: - Initialization - private init() {} + private init() { + Dependencies._lastCreatedInstance.mutate { $0 = self } + } internal init(forTesting: Bool) {} public static func createEmpty() -> Dependencies { return Dependencies() } @@ -104,26 +110,26 @@ public class Dependencies { } public func set(singleton: SingletonConfig, to instance: S) { - threadSafeChange(for: singleton.identifier) { + threadSafeChange(for: singleton.identifier, of: .singleton) { setValue(instance, typedStorage: .singleton(instance), key: singleton.identifier) } } public func set(cache: CacheConfig, to instance: M) { - threadSafeChange(for: cache.identifier) { + threadSafeChange(for: cache.identifier, of: .cache) { let value: Atomic = Atomic(cache.mutableInstance(instance)) setValue(value, typedStorage: .cache(value), key: cache.identifier) } } public func remove(cache: CacheConfig) { - threadSafeChange(for: cache.identifier) { - removeValue(cache.identifier) + threadSafeChange(for: cache.identifier, of: .cache) { + removeValue(cache.identifier, of: .cache) } } - public static func setIsRTLRetriever(requiresMainThread: Bool, isRTLRetriever: @escaping () -> Bool) { - _isRTLRetriever.mutate { $0 = (requiresMainThread, isRTLRetriever) } + public func setIsRTLRetriever(requiresMainThread: Bool, isRTLRetriever: @escaping () -> Bool) { + storage.mutate { $0.isRTLRetriever = (requiresMainThread, isRTLRetriever) } } } @@ -172,10 +178,10 @@ public extension Dependencies { } func set(feature: FeatureConfig, to updatedFeature: T?) { - threadSafeChange(for: feature.identifier) { + threadSafeChange(for: feature.identifier, of: .feature) { /// Update the cached & in-memory values let instance: Feature = ( - getValue(feature.identifier) ?? + getValue(feature.identifier, of: .feature) ?? feature.createInstance(self) ) instance.setValue(to: updatedFeature, using: self) @@ -187,11 +193,11 @@ public extension Dependencies { } func reset(feature: FeatureConfig) { - threadSafeChange(for: feature.identifier) { + threadSafeChange(for: feature.identifier, of: .feature) { /// Reset the cached and in-memory values - let instance: Feature? = getValue(feature.identifier) + let instance: Feature? = getValue(feature.identifier, of: .feature) instance?.setValue(to: nil, using: self) - removeValue(feature.identifier) + removeValue(feature.identifier, of: .feature) } /// Notify observers @@ -272,8 +278,31 @@ public enum DependenciesError: Error { private extension Dependencies { struct DependencyStorage { - var initializationLocks: [String: NSLock] = [:] - var instances: [String: Value] = [:] + var initializationLocks: [Key: NSLock] = [:] + var instances: [Key: Value] = [:] + var isRTLRetriever: (Bool, () -> Bool) = (false, { false }) + + struct Key: Hashable, CustomStringConvertible { + enum Variant: String { + case singleton + case cache + case userDefaults + case feature + + func key(_ identifier: String) -> Key { + return Key(identifier, of: self) + } + } + + let identifier: String + let variant: Variant + var description: String { "\(variant): \(identifier)" } + + init(_ identifier: String, of variant: Variant) { + self.identifier = identifier + self.variant = variant + } + } enum Value { case singleton(Any) @@ -281,6 +310,15 @@ private extension Dependencies { case userDefaults(UserDefaultsType) case feature(any FeatureType) + func distinctKey(for identifier: String) -> Key { + switch self { + case .singleton: return Key(identifier, of: .singleton) + case .cache: return Key(identifier, of: .cache) + case .userDefaults: return Key(identifier, of: .userDefaults) + case .feature: return Key(identifier, of: .feature) + } + } + func value(as type: T.Type) -> T? { switch self { case .singleton(let value): return value as? T @@ -329,14 +367,14 @@ private extension Dependencies { constructor: DependencyStorage.Constructor ) -> Value { /// If we already have an instance then just return that - if let existingValue: Value = getValue(identifier) { + if let existingValue: Value = getValue(identifier, of: constructor.variant) { return existingValue } - return threadSafeChange(for: identifier) { + return threadSafeChange(for: identifier, of: constructor.variant) { /// Now that we are within a synchronized group, check to make sure an instance wasn't created while we were waiting to /// enter the group - if let existingValue: Value = getValue(identifier) { + if let existingValue: Value = getValue(identifier, of: constructor.variant) { return existingValue } @@ -347,11 +385,11 @@ private extension Dependencies { } /// Convenience method to retrieve the existing dependency instance from memory in a thread-safe way - private func getValue(_ key: String) -> T? { - guard let typedValue: DependencyStorage.Value = storage.wrappedValue.instances[key] else { return nil } + private func getValue(_ key: String, of variant: DependencyStorage.Key.Variant) -> T? { + guard let typedValue: DependencyStorage.Value = storage.wrappedValue.instances[variant.key(key)] else { return nil } guard let result: T = typedValue.value(as: T.self) else { /// If there is a value stored for the key, but it's not the right type then something has gone wrong, and we should log - Log.critical("Failed to convert stored dependency '\(key)' to expected type: \(T.self)") + Log.critical("Failed to convert stored dependency '\(variant.key(key))' to expected type: \(T.self)") return nil } @@ -360,13 +398,13 @@ private extension Dependencies { /// Convenience method to store a dependency instance in memory in a thread-safe way @discardableResult private func setValue(_ value: T, typedStorage: DependencyStorage.Value, key: String) -> T { - storage.mutate { $0.instances[key] = typedStorage } + storage.mutate { $0.instances[typedStorage.distinctKey(for: key)] = typedStorage } return value } /// Convenience method to remove a dependency instance from memory in a thread-safe way - private func removeValue(_ key: String) { - storage.mutate { $0.instances.removeValue(forKey: key) } + private func removeValue(_ key: String, of variant: DependencyStorage.Key.Variant) { + storage.mutate { $0.instances.removeValue(forKey: variant.key(key)) } } /// This function creates an `NSLock` for the given identifier which allows us to block instance creation on a per-identifier basis @@ -374,14 +412,14 @@ private extension Dependencies { /// /// **Note:** This `NSLock` is an additional mechanism on top of the `Atomic` because the interface is a little simpler /// and we don't need to wrap every instance within `Atomic` this way - @discardableResult private func threadSafeChange(for identifier: String, change: () -> T) -> T { + @discardableResult private func threadSafeChange(for identifier: String, of variant: DependencyStorage.Key.Variant, change: () -> T) -> T { let lock: NSLock = storage.mutate { storage in - if let existing = storage.initializationLocks[identifier] { + if let existing = storage.initializationLocks[variant.key(identifier)] { return existing } let lock: NSLock = NSLock() - storage.initializationLocks[identifier] = lock + storage.initializationLocks[variant.key(identifier)] = lock return lock } lock.lock() @@ -395,10 +433,11 @@ private extension Dependencies { private extension Dependencies.DependencyStorage { struct Constructor { + let variant: Key.Variant let create: () -> (typedStorage: Dependencies.DependencyStorage.Value, value: T) static func singleton(_ constructor: @escaping () -> T) -> Constructor { - return Constructor { + return Constructor(variant: .singleton) { let instance: T = constructor() return (.singleton(instance), instance) @@ -406,7 +445,7 @@ private extension Dependencies.DependencyStorage { } static func cache(_ constructor: @escaping () -> T) -> Constructor where T: Atomic { - return Constructor { + return Constructor(variant: .cache) { let instance: T = constructor() return (.cache(instance), instance) @@ -414,7 +453,7 @@ private extension Dependencies.DependencyStorage { } static func userDefaults(_ constructor: @escaping () -> T) -> Constructor where T == UserDefaultsType { - return Constructor { + return Constructor(variant: .userDefaults) { let instance: T = constructor() return (.userDefaults(instance), instance) @@ -422,7 +461,7 @@ private extension Dependencies.DependencyStorage { } static func feature(_ constructor: @escaping () -> T) -> Constructor where T: FeatureType { - return Constructor { + return Constructor(variant: .feature) { let instance: T = constructor() return (.feature(instance), instance) diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index c4a27bfaa..0a6e2e494 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -9,6 +9,14 @@ public final class Features { } public extension FeatureStorage { + static let showStringKeys: FeatureConfig = Dependencies.create( + identifier: "showStringKeys" + ) + + static let forceOffline: FeatureConfig = Dependencies.create( + identifier: "forceOffline" + ) + static let debugDisappearingMessageDurations: FeatureConfig = Dependencies.create( identifier: "debugDisappearingMessageDurations" ) @@ -57,6 +65,14 @@ public extension FeatureStorage { static let updatedGroupsAllowInviteById: FeatureConfig = Dependencies.create( identifier: "updatedGroupsAllowInviteById" ) + + static let updatedGroupsDeleteBeforeNow: FeatureConfig = Dependencies.create( + identifier: "updatedGroupsDeleteBeforeNow" + ) + + static let updatedGroupsDeleteAttachmentsBeforeNow: FeatureConfig = Dependencies.create( + identifier: "updatedGroupsDeleteAttachmentsBeforeNow" + ) } // MARK: - FeatureOption diff --git a/SessionUtilitiesKit/General/Localization.swift b/SessionUtilitiesKit/General/Localization.swift index 693b2b239..f20f911f9 100644 --- a/SessionUtilitiesKit/General/Localization.swift +++ b/SessionUtilitiesKit/General/Localization.swift @@ -30,6 +30,10 @@ final public class LocalizationHelper: CustomStringConvertible { } public func localized() -> String { + guard !Dependencies.unsafeNonInjected[feature: .showStringKeys] else { + return "[\(template)]" + } + // Use English as the default string if the translation is empty let defaultString: String = { if let englishPath = Bundle.main.path(forResource: "en", ofType: "lproj"), let englishBundle = Bundle(path: englishPath) { diff --git a/SessionUtilitiesKit/General/UIEdgeInsets.swift b/SessionUtilitiesKit/General/UIEdgeInsets.swift index f904b923e..ef5777cda 100644 --- a/SessionUtilitiesKit/General/UIEdgeInsets.swift +++ b/SessionUtilitiesKit/General/UIEdgeInsets.swift @@ -8,9 +8,9 @@ extension UIEdgeInsets { public init(top: CGFloat, leading: CGFloat, bottom: CGFloat, trailing: CGFloat) { self.init( top: top, - left: (Dependencies.isRTL ? trailing : leading), + left: (Dependencies.unsafeNonInjected.isRTL ? trailing : leading), bottom: bottom, - right: (Dependencies.isRTL ? leading : trailing) + right: (Dependencies.unsafeNonInjected.isRTL ? leading : trailing) ) } } diff --git a/SessionUtilitiesKit/Media/UTType+Utilities.swift b/SessionUtilitiesKit/Media/UTType+Utilities.swift index 6ee882361..7765a2833 100644 --- a/SessionUtilitiesKit/Media/UTType+Utilities.swift +++ b/SessionUtilitiesKit/Media/UTType+Utilities.swift @@ -123,7 +123,7 @@ public extension UTType { let mimeType: String = preferredMIMEType, let fileExtension: String = UTType.genericExtensionTypesToMimeTypes .first(where: { _, value in value == mimeType })? - .value + .key else { return preferredFilenameExtension } return fileExtension diff --git a/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift b/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift index 00b246e35..99c01f854 100644 --- a/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift +++ b/SignalUtilitiesKit/Utilities/UIViewController+OWS.swift @@ -65,12 +65,12 @@ public extension UIViewController { let backButton: UIButton = UIButton(type: .custom) // Nudge closer to the left edge to match default back button item. - let extraLeftPadding: CGFloat = (Dependencies.isRTL ? 0 : -8) + let extraLeftPadding: CGFloat = (Dependencies.unsafeNonInjected.isRTL ? 0 : -8) // Give some extra hit area to the back button. This is a little smaller // than the default back button, but makes sense for our left aligned title // view in the MessagesViewController - let extraRightPadding: CGFloat = (Dependencies.isRTL ? -0 : 10) + let extraRightPadding: CGFloat = (Dependencies.unsafeNonInjected.isRTL ? -0 : 10) // Extra hit area above/below let extraHeightPadding: CGFloat = 8