From b7b7b4af6aeb27e22b1fa7cfd3356e160c9c5602 Mon Sep 17 00:00:00 2001
From: Morgan Pretty <morgan.t.pretty@gmail.com>
Date: Tue, 14 Nov 2023 11:55:31 +1100
Subject: [PATCH] Added disappearing messages support to updated groups

Added disappearing messages support to updated groups
Added the 10s & 60s debug disappearing message setting options to the DeveloperSettingsViewModel
Copy tweaks on the DeveloperSettingsViewModel
Removed some unused code
---
 ...isappearingMessagesSettingsViewModel.swift | 150 ++++++++++++---
 .../Settings/DeveloperSettingsViewModel.swift | 174 +++++++++++-------
 .../DisappearingMessageConfiguration.swift    |  63 ++-----
 .../Database/Models/Profile.swift             |  27 ---
 .../Networking/Feature+NetworkLayer.swift     |   4 +-
 SessionUtilitiesKit/General/Feature.swift     |  11 +-
 6 files changed, 253 insertions(+), 176 deletions(-)

diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift
index ae06539bb..0f07d8096 100644
--- a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift
+++ b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift
@@ -85,15 +85,18 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga
     
     let title: String = "DISAPPEARING_MESSAGES".localized()
     lazy var subtitle: String? = {
-        guard dependencies[feature: .updatedDisappearingMessages] else {
-            return (isNoteToSelf ? nil : "DISAPPERING_MESSAGES_SUBTITLE_CONTACTS".localized())
-        }
-        
-        if threadVariant == .contact && !isNoteToSelf {
-            return "DISAPPERING_MESSAGES_SUBTITLE_CONTACTS".localized()
+        switch (threadVariant, isNoteToSelf) {
+            case (.contact, false): return "DISAPPERING_MESSAGES_SUBTITLE_CONTACTS".localized()
+            case (.group, _): return "DISAPPERING_MESSAGES_SUBTITLE_GROUPS".localized()
+            case (.community, _): return nil
+                
+            case (.legacyGroup, _), (_, true):
+                guard dependencies[feature: .updatedDisappearingMessages] else {
+                    return (isNoteToSelf ? nil : "DISAPPERING_MESSAGES_SUBTITLE_CONTACTS".localized())
+                }
+                
+                return "DISAPPERING_MESSAGES_SUBTITLE_GROUPS".localized()
         }
-        
-        return "DISAPPERING_MESSAGES_SUBTITLE_GROUPS".localized()
     }()
     
     lazy var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> = configSubject
@@ -272,7 +275,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga
                                         }
 
                                         return (currentConfig.type ?? .disappearAfterSend)
-                                    }())
+                                    }(), using: dependencies)
                                     .map { duration in
                                         let title: String = duration.formatted(format: .long)
 
@@ -302,8 +305,77 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga
                             )
                         )
                     ].compactMap { $0 }
+                    
+                case (.group, _):
+                    return [
+                        SectionModel(
+                            model: .group,
+                            elements: [
+                                SessionCell.Info(
+                                    id: "DISAPPEARING_MESSAGES_OFF".localized(),
+                                    title: "DISAPPEARING_MESSAGES_OFF".localized(),
+                                    trailingAccessory: .radio(
+                                        isSelected: !currentConfig.isEnabled
+                                    ),
+                                    isEnabled: (currentUserIsClosedGroupAdmin == true),
+                                    accessibility: Accessibility(
+                                        identifier: "Disable disappearing messages (Off option)",
+                                        label: "Disable disappearing messages (Off option)"
+                                    ),
+                                    onTap: {
+                                        self?.configSubject.send(
+                                            currentConfig.with(
+                                                isEnabled: false,
+                                                durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.off.seconds,
+                                                lastChangeTimestampMs: SnodeAPI.currentOffsetTimestampMs()
+                                            )
+                                        )
+                                    }
+                                )
+                            ]
+                            .appending(
+                                contentsOf: DisappearingMessagesConfiguration
+                                    .validDurationsSeconds(.disappearAfterSend, using: dependencies)
+                                    .map { duration in
+                                        let title: String = duration.formatted(format: .long)
 
-                case (.legacyGroup, _), (.group, _), (_, true):
+                                        return SessionCell.Info(
+                                            id: title,
+                                            title: title,
+                                            trailingAccessory: .radio(
+                                                isSelected: (
+                                                    currentConfig.isEnabled &&
+                                                    currentConfig.durationSeconds == duration
+                                                )
+                                            ),
+                                            isEnabled: (currentUserIsClosedGroupAdmin == true),
+                                            accessibility: Accessibility(
+                                                identifier: "Time option",
+                                                label: "Time option"
+                                            ),
+                                            onTap: {
+                                                // If the new disappearing messages config feature flag isn't
+                                                // enabled then the 'isEnabled' and 'type' values are set via
+                                                // the first section so pass `nil` values to keep the existing
+                                                // setting
+                                                self?.configSubject.send(
+                                                    currentConfig.with(
+                                                        isEnabled: true,
+                                                        durationSeconds: duration,
+                                                        type: .disappearAfterSend,
+                                                        lastChangeTimestampMs: SnodeAPI.currentOffsetTimestampMs(
+                                                            using: dependencies
+                                                        )
+                                                    )
+                                                )
+                                            }
+                                        )
+                                    }
+                            )
+                        )
+                    ]
+
+                case (.legacyGroup, _), (_, true):
                     return [
                         (dependencies[feature: .updatedDisappearingMessages] ? nil :
                             SectionModel(
@@ -415,7 +487,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga
                                 .compactMap { $0 }
                                 .appending(
                                     contentsOf: DisappearingMessagesConfiguration
-                                        .validDurationsSeconds(.disappearAfterSend)
+                                        .validDurationsSeconds(.disappearAfterSend, using: dependencies)
                                         .map { duration in
                                             let title: String = duration.formatted(format: .long)
 
@@ -483,7 +555,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga
                 .filter(Interaction.Columns.variant == Interaction.Variant.infoDisappearingMessagesUpdate)
                 .deleteAll(db)
             
-            let currentOffsetTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
+            let currentOffsetTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs(using: dependencies)
             
             let interaction: Interaction = try Interaction(
                 threadId: threadId,
@@ -496,22 +568,46 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga
             )
             .inserted(db)
             
-            let duration: UInt32? = {
-                guard !dependencies[feature: .updatedDisappearingMessages] else { return nil }
-                return UInt32(floor(updatedConfig.isEnabled ? updatedConfig.durationSeconds : 0))
-            }()
+            // Send a control message that the disappearing messages setting changed
+            switch threadVariant {
+                case .group:
+                    try MessageSender.send(
+                        db,
+                        message: GroupUpdateInfoChangeMessage(
+                            changeType: .disappearingMessages,
+                            updatedExpiration: UInt32(updatedConfig.isEnabled ? updatedConfig.durationSeconds : 0),
+                            sentTimestamp: UInt64(currentOffsetTimestampMs),
+                            authMethod: try Authentication.with(
+                                db,
+                                sessionIdHexString: threadId,
+                                using: dependencies
+                            ),
+                            using: dependencies
+                        ),
+                        interactionId: nil,
+                        threadId: threadId,
+                        threadVariant: .group,
+                        using: dependencies
+                    )
+                    
+                default:
+                    let duration: UInt32? = {
+                        guard !dependencies[feature: .updatedDisappearingMessages] else { return nil }
+                        return UInt32(floor(updatedConfig.isEnabled ? updatedConfig.durationSeconds : 0))
+                    }()
 
-            try MessageSender.send(
-                db,
-                message: ExpirationTimerUpdate(
-                    syncTarget: nil,
-                    duration: duration
-                ),
-                interactionId: interaction.id,
-                threadId: threadId,
-                threadVariant: threadVariant,
-                using: dependencies
-            )
+                    try MessageSender.send(
+                        db,
+                        message: ExpirationTimerUpdate(
+                            syncTarget: nil,
+                            duration: duration
+                        ),
+                        interactionId: interaction.id,
+                        threadId: threadId,
+                        threadVariant: threadVariant,
+                        using: dependencies
+                    )
+            }
         }
         
         // Contacts & legacy closed groups need to update the SessionUtil
diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift
index c27e53346..d367eda4c 100644
--- a/Session/Settings/DeveloperSettingsViewModel.swift
+++ b/Session/Settings/DeveloperSettingsViewModel.swift
@@ -13,8 +13,6 @@ import SessionUtilitiesKit
 import SignalCoreKit
 
 class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource {
-    typealias TableItem = Section
-    
     public let dependencies: Dependencies
     public let navigatableState: NavigatableState = NavigatableState()
     public let state: TableDataState<Section, TableItem> = TableDataState()
@@ -30,29 +28,61 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
     
     // MARK: - Section
     
-    public enum Section: SessionTableSection, CaseIterable {
+    public enum Section: SessionTableSection {
+        case developerMode
+        case network
+        case disappearingMessages
+        case groups
+        case database
+        
+        var title: String? {
+            switch self {
+                case .developerMode: return nil
+                case .network: return "Network"
+                case .disappearingMessages: return "Disappearing Messages"
+                case .groups: return "Groups"
+                case .database: return "Database"
+            }
+        }
+        //default: return .titleRoundedContent // .padding
+        var style: SessionTableSectionStyle {
+            switch self {
+                case .developerMode: return .padding
+                default: return .titleRoundedContent
+            }
+        }
+    }
+    
+    public enum TableItem: Differentiable, CaseIterable {
         case developerMode
+        
         case serviceNetwork
         case networkLayer
+        
         case updatedDisappearingMessages
+        case debugDisappearingMessageDurations
+        
         case updatedGroups
         case updatedGroupsRemoveMessagesOnKick
         case updatedGroupsAllowHistoricAccessOnInvite
         case updatedGroupsAllowDisplayPicture
         case updatedGroupsAllowDescriptionEditing
         case updatedGroupsAllowPromotions
-        case exportDatabase
         
-        var style: SessionTableSectionStyle { .padding }
+        case exportDatabase
     }
     
     // MARK: - Content
     
     private struct State: Equatable {
         let developerMode: Bool
+        
         let serviceNetwork: ServiceNetwork
         let networkLayer: Network.Layers
+        
+        let debugDisappearingMessageDurations: Bool
         let updatedDisappearingMessages: Bool
+        
         let updatedGroups: Bool
         let updatedGroupsRemoveMessagesOnKick: Bool
         let updatedGroupsAllowHistoricAccessOnInvite: Bool
@@ -69,6 +99,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                 developerMode: dependencies[singleton: .storage, key: .developerModeEnabled],
                 serviceNetwork: dependencies[feature: .serviceNetwork],
                 networkLayer: dependencies[feature: .networkLayers],
+                debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations],
                 updatedDisappearingMessages: dependencies[feature: .updatedDisappearingMessages],
                 updatedGroups: dependencies[feature: .updatedGroups],
                 updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick],
@@ -87,9 +118,11 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                             id: .developerMode,
                             title: "Developer Mode",
                             subtitle: """
-                            Developer Mode grants the device access to the settings on this screen.
+                            Grants access to this screen.
                             
-                            Disabling this setting will reset all of the below settings back to default (removing data as described below) and revoke access to this screen unless Developer Mode is re-enabled.
+                            Disabling this setting will:
+                            • Reset all the below settings to default (removing data as described below)
+                            • Revoke access to this screen unless Developer Mode is re-enabled
                             """,
                             trailingAccessory: .toggle(
                                 .boolValue(current.developerMode, oldValue: (previous ?? current).developerMode)
@@ -103,15 +136,16 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                     ]
                 ),
                 SectionModel(
-                    model: .serviceNetwork,
+                    model: .network,
                     elements: [
                         SessionCell.Info(
                             id: .serviceNetwork,
-                            title: "Network",
+                            title: "Environment",
                             subtitle: """
-                            The service network which should be used for sending requests and storing messages.
+                            The environment used for sending requests and storing messages.
                             
-                            <b>Warning:</b> These networks cannot communicate with each other so changing this network will result in all conversation and snode data being cleared and any pending network requests being cancelled.
+                            <b>Warning:</b>
+                            Changing this setting will result in all conversation and snode data being cleared and any pending network requests being cancelled.
                             """,
                             trailingAccessory: .dropDown(
                                 .dynamicString { current.serviceNetwork.title }
@@ -120,7 +154,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                                 self?.transitionToScreen(
                                     SessionTableViewController(
                                         viewModel: SessionListViewModel<ServiceNetwork>(
-                                            title: "Network",
+                                            title: "Environment",
                                             options: ServiceNetwork.allCases,
                                             behaviour: .autoDismiss(
                                                 initialSelection: current.serviceNetwork,
@@ -131,19 +165,17 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                                     )
                                 )
                             }
-                        )
-                    ]
-                ),
-                SectionModel(
-                    model: .networkLayer,
-                    elements: [
+                        ),
                         SessionCell.Info(
                             id: .networkLayer,
-                            title: "Network Layer",
+                            title: "Routing",
                             subtitle: """
-                            The network layer which all network traffic should be routed through. We do support sending network traffic through multiple network layers, if multiple layers are selected then requests will wait for a response from all layers before completing with the first successful response.
+                            The network layer which all network traffic should be routed through.
+                            
+                            We do support sending network traffic through multiple network layers, if multiple layers are selected then requests will wait for a response from all layers before completing with the first successful response.
                             
-                            <b>Warning:</b> Different network layers offer different levels of privacy, make sure to read the description of the network layers before making a selection.
+                            <b>Warning:</b>
+                            Different network layers offer different levels of privacy, make sure to read the description of the network layers before making a selection.
                             """,
                             trailingAccessory: .dropDown(
                                 .dynamicString { current.networkLayer.title }
@@ -152,7 +184,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                                 self?.transitionToScreen(
                                     SessionTableViewController(
                                         viewModel: SessionListViewModel<Network.Layers>(
-                                            title: "Network Layer",
+                                            title: "Routing",
                                             options: Network.Layers.allCases,
                                             behaviour: .singleSelect(
                                                 initialSelection: current.networkLayer,
@@ -167,13 +199,34 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                     ]
                 ),
                 SectionModel(
-                    model: .updatedDisappearingMessages,
+                    model: .disappearingMessages,
                     elements: [
+                        SessionCell.Info(
+                            id: .debugDisappearingMessageDurations,
+                            title: "Debug Durations",
+                            subtitle: """
+                            Adds 10 and 60 second durations for Disappearing Message settings.
+                            
+                            These should only be used for debugging purposes and can result in odd behaviours.
+                            """,
+                            trailingAccessory: .toggle(
+                                .boolValue(
+                                    current.debugDisappearingMessageDurations,
+                                    oldValue: (previous ?? current).debugDisappearingMessageDurations
+                                )
+                            ),
+                            onTap: {
+                                self?.updateFlag(
+                                    for: .debugDisappearingMessageDurations,
+                                    to: !current.debugDisappearingMessageDurations
+                                )
+                            }
+                        ),
                         SessionCell.Info(
                             id: .updatedDisappearingMessages,
-                            title: "Updated Disappearing Messages",
+                            title: "Use Updated Disappearing Messages",
                             subtitle: """
-                            This setting controls whether legacy or updated disappearing messages should be used.
+                            Controls whether legacy or updated disappearing messages should be used.
                             """,
                             trailingAccessory: .toggle(
                                 .boolValue(
@@ -191,29 +244,24 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                     ]
                 ),
                 SectionModel(
-                    model: .updatedGroups,
+                    model: .groups,
                     elements: [
                         SessionCell.Info(
                             id: .updatedGroups,
-                            title: "Updated Groups",
+                            title: "Use Updated Groups",
                             subtitle: """
-                            This settings controls whether newly created groups should use the updated groups or legacy groups.
+                            Controls whether newly created groups are updated or legacy groups.
                             """,
                             trailingAccessory: .toggle(
                                 .boolValue(current.updatedGroups, oldValue: (previous ?? current).updatedGroups)
                             ),
                             onTap: { self?.updateFlag(for: .updatedGroups, to: !current.updatedGroups) }
-                        )
-                    ]
-                ),
-                SectionModel(
-                    model: .updatedGroupsRemoveMessagesOnKick,
-                    elements: [
+                        ),
                         SessionCell.Info(
                             id: .updatedGroupsRemoveMessagesOnKick,
-                            title: "Remove Messages when Kicking from Updated Groups",
+                            title: "Remove Messages on Kick",
                             subtitle: """
-                            This settings controls whether a group members messages should be removed when they are kicked from an updated group.
+                            Controls whether a group members messages should be removed when they are kicked from an updated group.
                             
                             <b>Note:</b> In a future release we will offer this as an option when removing members but for the initial release it can be controlled via this flag for testing purposes.
                             """,
@@ -229,17 +277,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                                     to: !current.updatedGroupsRemoveMessagesOnKick
                                 )
                             }
-                        )
-                    ]
-                ),
-                SectionModel(
-                    model: .updatedGroupsAllowHistoricAccessOnInvite,
-                    elements: [
+                        ),
                         SessionCell.Info(
                             id: .updatedGroupsAllowHistoricAccessOnInvite,
-                            title: "Allow access to historic messages when inviting to an updated group",
+                            title: "Allow Historic Message Access",
                             subtitle: """
-                            This settings controls whether a group members should be granted access to hsitoric messages when invited to an updated group.
+                            Controls whether members should be granted access to historic messages when invited to an updated group.
                             
                             <b>Note:</b> In a future release we will offer this as an option when inviting members but for the initial release it can be controlled via this flag for testing purposes.
                             """,
@@ -255,17 +298,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                                     to: !current.updatedGroupsAllowHistoricAccessOnInvite
                                 )
                             }
-                        )
-                    ]
-                ),
-                SectionModel(
-                    model: .updatedGroupsAllowDisplayPicture,
-                    elements: [
+                        ),
                         SessionCell.Info(
                             id: .updatedGroupsAllowDisplayPicture,
-                            title: "Shows UI for setting updated group custom display pictures",
+                            title: "Custom Display Pictures",
                             subtitle: """
-                            This settings controls whether the UI allows group admins to set a custom display picture for a group.
+                            Controls whether the UI allows group admins to set a custom display picture for a group.
                             
                             <b>Note:</b> In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes.
                             """,
@@ -281,17 +319,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                                     to: !current.updatedGroupsAllowDisplayPicture
                                 )
                             }
-                        )
-                    ]
-                ),
-                SectionModel(
-                    model: .updatedGroupsAllowDescriptionEditing,
-                    elements: [
+                        ),
                         SessionCell.Info(
                             id: .updatedGroupsAllowDescriptionEditing,
-                            title: "Show UI for editing updated group descriptions",
+                            title: "Edit Group Descriptions",
                             subtitle: """
-                            This settings controls whether the UI allows group admins to modify the descriptions of updated groups.
+                            Controls whether the UI allows group admins to modify the descriptions of updated groups.
                             
                             <b>Note:</b> In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes.
                             """,
@@ -307,17 +340,12 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                                     to: !current.updatedGroupsAllowDescriptionEditing
                                 )
                             }
-                        )
-                    ]
-                ),
-                SectionModel(
-                    model: .updatedGroupsAllowPromotions,
-                    elements: [
+                        ),
                         SessionCell.Info(
                             id: .updatedGroupsAllowPromotions,
-                            title: "Show UI for updated group promotions",
+                            title: "Allow Group Promotions",
                             subtitle: """
-                            This settings controls whether the UI allows group admins promote other group members to admin within an updated group.
+                            Controls whether the UI allows group admins promote other group members to admin within an updated group.
                             
                             <b>Note:</b> In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes.
                             """,
@@ -337,7 +365,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
                     ]
                 ),
                 SectionModel(
-                    model: .exportDatabase,
+                    model: .database,
                     elements: [
                         SessionCell.Info(
                             id: .exportDatabase,
@@ -362,14 +390,18 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
     private func disableDeveloperMode() {
         /// Loop through all of the sections and reset the features back to default for each one as needed (this way if a new section is added
         /// then we will get a compile error if it doesn't get resetting instructions added)
-        Section.allCases.forEach { section in
-            switch section {
+        TableItem.allCases.forEach { item in
+            switch item {
                 case .developerMode: break  // Not a feature
                 case .exportDatabase: break  // Not a feature
                 
                 case .serviceNetwork: updateServiceNetwork(to: nil)
                 case .networkLayer: updateNetworkLayers(to: nil)
+                    
+                case .debugDisappearingMessageDurations:
+                    updateFlag(for: .debugDisappearingMessageDurations, to: nil)
                 case .updatedDisappearingMessages: updateFlag(for: .updatedDisappearingMessages, to: nil)
+                    
                 case .updatedGroups: updateFlag(for: .updatedGroups, to: nil)
                 case .updatedGroupsRemoveMessagesOnKick: updateFlag(for: .updatedGroupsRemoveMessagesOnKick, to: nil)
                 case .updatedGroupsAllowHistoricAccessOnInvite:
diff --git a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift
index c493df230..53e07df32 100644
--- a/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift
+++ b/SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift
@@ -247,32 +247,15 @@ public extension DisappearingMessagesConfiguration {
 // MARK: - UI Constraints
 
 extension DisappearingMessagesConfiguration {
-    // TODO: Remove this when disappearing messages V2 is up and running
-    public static var validDurationsSeconds: [TimeInterval] {
-        return [
-            5,
-            10,
-            30,
-            (1 * 60),
-            (5 * 60),
-            (30 * 60),
-            (1 * 60 * 60),
-            (6 * 60 * 60),
-            (12 * 60 * 60),
-            (24 * 60 * 60),
-            (7 * 24 * 60 * 60)
-        ]
-    }
-    
-    public static var maxDurationSeconds: TimeInterval = {
-        return (validDurationsSeconds.max() ?? 0)
-    }()
-    
-    public static func validDurationsSeconds(_ type: DisappearingMessageType) -> [TimeInterval] {
-        
+    public static func validDurationsSeconds(
+        _ type: DisappearingMessageType,
+        using dependencies: Dependencies
+    ) -> [TimeInterval] {
         switch type {
             case .disappearAfterRead:
-                var result =  [
+                return [
+                    (dependencies[feature: .debugDisappearingMessageDurations] ? 10 : nil),
+                    (dependencies[feature: .debugDisappearingMessageDurations] ? 60 : nil),
                     (5 * 60),
                     (1 * 60 * 60),
                     (12 * 60 * 60),
@@ -280,35 +263,19 @@ extension DisappearingMessagesConfiguration {
                     (7 * 24 * 60 * 60),
                     (2 * 7 * 24 * 60 * 60)
                 ]
-                .map { TimeInterval($0)  }
-                #if targetEnvironment(simulator)
-                    result.insert(
-                        TimeInterval(60),
-                        at: 0
-                    )
-                    result.insert(
-                        TimeInterval(10),
-                        at: 0
-                    )
-                #endif
-                return result
+                .compactMap { duration in duration.map { TimeInterval($0) } }
+                
             case .disappearAfterSend:
-                var result =  [
+                return [
+                    (dependencies[feature: .debugDisappearingMessageDurations] ? 10 : nil),
                     (12 * 60 * 60),
                     (24 * 60 * 60),
                     (7 * 24 * 60 * 60),
                     (2 * 7 * 24 * 60 * 60)
                 ]
-                .map { TimeInterval($0)  }
-                #if targetEnvironment(simulator)
-                    result.insert(
-                        TimeInterval(10),
-                        at: 0
-                    )
-                #endif
-                return result
-            default:
-                return []
-            }
+                .compactMap { duration in duration.map { TimeInterval($0) } }
+                
+            default: return []
+        }
     }
 }
diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift
index 77bf7d724..0b5a3baab 100644
--- a/SessionMessagingKit/Database/Models/Profile.swift
+++ b/SessionMessagingKit/Database/Models/Profile.swift
@@ -172,33 +172,6 @@ public extension Profile {
 // MARK: - Protobuf
 
 public extension Profile {
-    static func fromProto(_ proto: SNProtoDataMessage, id: String) -> Profile? {
-        guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
-        
-        var profileKey: Data?
-        var profilePictureUrl: String?
-        let sentTimestamp: TimeInterval = TimeInterval(proto.hasTimestamp ? (Double(proto.timestamp) / 1000) : 0)
-        
-        // If we have both a `profileKey` and a `profilePicture` then the key MUST be valid
-        if let profileKeyData: Data = proto.profileKey, profileProto.profilePicture != nil {
-            profileKey = profileKeyData
-            profilePictureUrl = profileProto.profilePicture
-        }
-        
-        return Profile(
-            id: id,
-            name: displayName,
-            lastNameUpdate: sentTimestamp,
-            nickname: nil,
-            profilePictureUrl: profilePictureUrl,
-            profilePictureFileName: nil,
-            profileEncryptionKey: profileKey,
-            lastProfilePictureUpdate: sentTimestamp,
-            blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil),
-            lastBlocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? sentTimestamp : nil)
-        )
-    }
-
     func toProto() -> SNProtoDataMessage? {
         let dataMessageProto = SNProtoDataMessage.builder()
         let profileProto = SNProtoLokiProfile.builder()
diff --git a/SessionSnodeKit/Networking/Feature+NetworkLayer.swift b/SessionSnodeKit/Networking/Feature+NetworkLayer.swift
index 8bb969dc4..fc5cff450 100644
--- a/SessionSnodeKit/Networking/Feature+NetworkLayer.swift
+++ b/SessionSnodeKit/Networking/Feature+NetworkLayer.swift
@@ -74,12 +74,12 @@ public extension Network {
             switch self {
                 case .onionRequest:
                     return """
-                    This network layer will send requests via the original Onion Request mechanism, requests will be routed between 3 service nodes before reaching their destination.
+                    Requests will be sent via the original Onion Request mechanism, they will be routed between 3 service nodes before reaching their destination.
                     """
                     
                 case .direct:
                     return """
-                    This network layer will send requests directly over HTTPS
+                    Requests will be sent directly over HTTPS.
                     
                     <b>Warning:</b> This network layer offers no IP protections so should only be used for debugging purposes.
                     """
diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift
index 3d622a0b6..e47e15c65 100644
--- a/SessionUtilitiesKit/General/Feature.swift
+++ b/SessionUtilitiesKit/General/Feature.swift
@@ -9,6 +9,10 @@ public final class Features {
 }
 
 public extension FeatureStorage {
+    static let debugDisappearingMessageDurations: FeatureConfig<Bool> = Dependencies.create(
+        identifier: "debugDisappearingMessageDurations"
+    )
+    
     static let updatedDisappearingMessages: FeatureConfig<Bool> = Dependencies.create(
         identifier: "updatedDisappearingMessages",
         automaticChangeBehaviour: Feature<Bool>.ChangeBehaviour(
@@ -53,10 +57,15 @@ public protocol FeatureOption: RawRepresentable, CaseIterable, Equatable where R
     
     static var defaultOption: Self { get }
     
+    var isValidOption: Bool { get }
     var title: String { get }
     var subtitle: String? { get }
 }
 
+public extension FeatureOption {
+    var isValidOption: Bool { true }
+}
+
 // MARK: - FeatureEvent
 
 public protocol FeatureEvent: Equatable, Hashable {
@@ -116,7 +125,7 @@ public struct Feature<T: FeatureOption>: FeatureType {
         
         /// If we have an explicitly set `selectedOption` then we should use that, otherwise we should check if any of the
         /// `automaticChangeBehaviour` conditions have been met, and if so use the specified value
-        guard let selectedOption: T = maybeSelectedOption else {
+        guard let selectedOption: T = maybeSelectedOption, selectedOption.isValidOption else {
             func automaticChangeConditionMet(_ condition: ChangeCondition) -> Bool {
                 switch condition {
                     case .after(let timestamp): return (dependencies.dateNow.timeIntervalSince1970 >= timestamp)