mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			303 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			303 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import Foundation
 | 
						|
import Combine
 | 
						|
import GRDB
 | 
						|
import LocalAuthentication
 | 
						|
import DifferenceKit
 | 
						|
import SessionUIKit
 | 
						|
import SessionMessagingKit
 | 
						|
import SessionUtilitiesKit
 | 
						|
 | 
						|
class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, NavigatableStateHolder, ObservableTableSource {
 | 
						|
    public let dependencies: Dependencies
 | 
						|
    public let navigatableState: NavigatableState = NavigatableState()
 | 
						|
    public let editableState: EditableState<TableItem> = EditableState()
 | 
						|
    public let state: TableDataState<Section, TableItem> = TableDataState()
 | 
						|
    public let observableState: ObservableTableSourceState<Section, TableItem> = ObservableTableSourceState()
 | 
						|
    private let shouldShowCloseButton: Bool
 | 
						|
    
 | 
						|
    // MARK: - Initialization
 | 
						|
    
 | 
						|
    init(shouldShowCloseButton: Bool = false, using dependencies: Dependencies = Dependencies()) {
 | 
						|
        self.dependencies = dependencies
 | 
						|
        self.shouldShowCloseButton = shouldShowCloseButton
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - Config
 | 
						|
    
 | 
						|
    enum NavItem: Equatable {
 | 
						|
        case close
 | 
						|
    }
 | 
						|
    
 | 
						|
    public enum Section: SessionTableSection {
 | 
						|
        case screenSecurity
 | 
						|
        case messageRequests
 | 
						|
        case readReceipts
 | 
						|
        case typingIndicators
 | 
						|
        case linkPreviews
 | 
						|
        case calls
 | 
						|
        
 | 
						|
        var title: String? {
 | 
						|
            switch self {
 | 
						|
                case .screenSecurity: return "PRIVACY_SECTION_SCREEN_SECURITY".localized()
 | 
						|
                case .messageRequests: return "PRIVACY_SECTION_MESSAGE_REQUESTS".localized()
 | 
						|
                case .readReceipts: return "PRIVACY_SECTION_READ_RECEIPTS".localized()
 | 
						|
                case .typingIndicators: return "PRIVACY_SECTION_TYPING_INDICATORS".localized()
 | 
						|
                case .linkPreviews: return "PRIVACY_SECTION_LINK_PREVIEWS".localized()
 | 
						|
                case .calls: return "PRIVACY_SECTION_CALLS".localized()
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        var style: SessionTableSectionStyle { return .titleRoundedContent }
 | 
						|
    }
 | 
						|
    
 | 
						|
    public enum TableItem: Differentiable {
 | 
						|
        case screenLock
 | 
						|
        case communityMessageRequests
 | 
						|
        case screenshotNotifications
 | 
						|
        case readReceipts
 | 
						|
        case typingIndicators
 | 
						|
        case linkPreviews
 | 
						|
        case calls
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - Navigation
 | 
						|
    
 | 
						|
    lazy var leftNavItems: AnyPublisher<[SessionNavItem<NavItem>], Never> = (!shouldShowCloseButton ? [] :
 | 
						|
        [
 | 
						|
            SessionNavItem(
 | 
						|
                id: .close,
 | 
						|
                image: UIImage(named: "X")?
 | 
						|
                    .withRenderingMode(.alwaysTemplate),
 | 
						|
                style: .plain,
 | 
						|
                accessibilityIdentifier: "Close button"
 | 
						|
            ) { [weak self] in self?.dismissScreen() }
 | 
						|
        ]
 | 
						|
    )
 | 
						|
    
 | 
						|
    // MARK: - Content
 | 
						|
    
 | 
						|
    private struct State: Equatable {
 | 
						|
        let isScreenLockEnabled: Bool
 | 
						|
        let checkForCommunityMessageRequests: Bool
 | 
						|
        let areReadReceiptsEnabled: Bool
 | 
						|
        let typingIndicatorsEnabled: Bool
 | 
						|
        let areLinkPreviewsEnabled: Bool
 | 
						|
        let areCallsEnabled: Bool
 | 
						|
    }
 | 
						|
    
 | 
						|
    let title: String = "PRIVACY_TITLE".localized()
 | 
						|
    
 | 
						|
    lazy var observation: TargetObservation = ObservationBuilder
 | 
						|
        .databaseObservation(self) { [weak self] db -> State in
 | 
						|
            State(
 | 
						|
                isScreenLockEnabled: db[.isScreenLockEnabled],
 | 
						|
                checkForCommunityMessageRequests: db[.checkForCommunityMessageRequests],
 | 
						|
                areReadReceiptsEnabled: db[.areReadReceiptsEnabled],
 | 
						|
                typingIndicatorsEnabled: db[.typingIndicatorsEnabled],
 | 
						|
                areLinkPreviewsEnabled: db[.areLinkPreviewsEnabled],
 | 
						|
                areCallsEnabled: db[.areCallsEnabled]
 | 
						|
            )
 | 
						|
        }
 | 
						|
        .mapWithPrevious { [dependencies] previous, current -> [SectionModel] in
 | 
						|
            return [
 | 
						|
                SectionModel(
 | 
						|
                    model: .screenSecurity,
 | 
						|
                    elements: [
 | 
						|
                        SessionCell.Info(
 | 
						|
                            id: .screenLock,
 | 
						|
                            title: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE".localized(),
 | 
						|
                            subtitle: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION".localized(),
 | 
						|
                            rightAccessory: .toggle(
 | 
						|
                                .boolValue(
 | 
						|
                                    key: .isScreenLockEnabled,
 | 
						|
                                    value: current.isScreenLockEnabled,
 | 
						|
                                    oldValue: (previous ?? current).isScreenLockEnabled
 | 
						|
                                )
 | 
						|
                            ),
 | 
						|
                            onTap: { [weak self] in
 | 
						|
                                // Make sure the device has a passcode set before allowing screen lock to
 | 
						|
                                // be enabled (Note: This will always return true on a simulator)
 | 
						|
                                guard LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) else {
 | 
						|
                                    self?.transitionToScreen(
 | 
						|
                                        ConfirmationModal(
 | 
						|
                                            info: ConfirmationModal.Info(
 | 
						|
                                                title: "SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE".localized(),
 | 
						|
                                                cancelTitle: "BUTTON_OK".localized(),
 | 
						|
                                                cancelStyle: .alert_text
 | 
						|
                                            )
 | 
						|
                                        ),
 | 
						|
                                        transitionType: .present
 | 
						|
                                    )
 | 
						|
                                    return
 | 
						|
                                }
 | 
						|
                                
 | 
						|
                                Storage.shared.write { db in
 | 
						|
                                    try db.setAndUpdateConfig(.isScreenLockEnabled, to: !db[.isScreenLockEnabled])
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                        )
 | 
						|
                    ]
 | 
						|
                ),
 | 
						|
                SectionModel(
 | 
						|
                    model: .messageRequests,
 | 
						|
                    elements: [
 | 
						|
                        SessionCell.Info(
 | 
						|
                            id: .communityMessageRequests,
 | 
						|
                            title: "PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE".localized(),
 | 
						|
                            subtitle: "PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION".localized(),
 | 
						|
                            rightAccessory: .toggle(
 | 
						|
                                .boolValue(
 | 
						|
                                    key: .checkForCommunityMessageRequests,
 | 
						|
                                    value: current.checkForCommunityMessageRequests,
 | 
						|
                                    oldValue: (previous ?? current).checkForCommunityMessageRequests
 | 
						|
                                )
 | 
						|
                            ),
 | 
						|
                            onTap: { [weak self] in
 | 
						|
                                Storage.shared.write { db in
 | 
						|
                                    try db.setAndUpdateConfig(
 | 
						|
                                        .checkForCommunityMessageRequests,
 | 
						|
                                        to: !db[.checkForCommunityMessageRequests]
 | 
						|
                                    )
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                        )
 | 
						|
                    ]
 | 
						|
                ),
 | 
						|
                SectionModel(
 | 
						|
                    model: .readReceipts,
 | 
						|
                    elements: [
 | 
						|
                        SessionCell.Info(
 | 
						|
                            id: .readReceipts,
 | 
						|
                            title: "PRIVACY_READ_RECEIPTS_TITLE".localized(),
 | 
						|
                            subtitle: "PRIVACY_READ_RECEIPTS_DESCRIPTION".localized(),
 | 
						|
                            rightAccessory: .toggle(
 | 
						|
                                .boolValue(
 | 
						|
                                    key: .areReadReceiptsEnabled,
 | 
						|
                                    value: current.areReadReceiptsEnabled,
 | 
						|
                                    oldValue: (previous ?? current).areReadReceiptsEnabled
 | 
						|
                                )
 | 
						|
                            ),
 | 
						|
                            onTap: {
 | 
						|
                                Storage.shared.write { db in
 | 
						|
                                    try db.setAndUpdateConfig(.areReadReceiptsEnabled, to: !db[.areReadReceiptsEnabled])
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                        )
 | 
						|
                    ]
 | 
						|
                ),
 | 
						|
                SectionModel(
 | 
						|
                    model: .typingIndicators,
 | 
						|
                    elements: [
 | 
						|
                        SessionCell.Info(
 | 
						|
                            id: .typingIndicators,
 | 
						|
                            title: SessionCell.TextInfo(
 | 
						|
                                "PRIVACY_TYPING_INDICATORS_TITLE".localized(),
 | 
						|
                                font: .title
 | 
						|
                            ),
 | 
						|
                            subtitle: SessionCell.TextInfo(
 | 
						|
                                "PRIVACY_TYPING_INDICATORS_DESCRIPTION".localized(),
 | 
						|
                                font: .subtitle,
 | 
						|
                                extraViewGenerator: {
 | 
						|
                                    let targetHeight: CGFloat = 20
 | 
						|
                                    let targetWidth: CGFloat = ceil(20 * (targetHeight / 12))
 | 
						|
                                    let result: UIView = UIView(
 | 
						|
                                        frame: CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight)
 | 
						|
                                    )
 | 
						|
                                    result.set(.width, to: targetWidth)
 | 
						|
                                    result.set(.height, to: targetHeight)
 | 
						|
                                    
 | 
						|
                                    // Use a transform scale to reduce the size of the typing indicator to the
 | 
						|
                                    // desired size (this way the animation remains intact)
 | 
						|
                                    let cell: TypingIndicatorCell = TypingIndicatorCell()
 | 
						|
                                    cell.transform = CGAffineTransform.scale(targetHeight / cell.bounds.height)
 | 
						|
                                    cell.typingIndicatorView.startAnimation()
 | 
						|
                                    result.addSubview(cell)
 | 
						|
                                    
 | 
						|
                                    // Note: Because we are messing with the transform these values don't work
 | 
						|
                                    // logically so we inset the positioning to make it look visually centered
 | 
						|
                                    // within the layout inspector
 | 
						|
                                    cell.center(.vertical, in: result, withInset: -(targetHeight * 0.15))
 | 
						|
                                    cell.center(.horizontal, in: result, withInset: -(targetWidth * 0.35))
 | 
						|
                                    cell.set(.width, to: .width, of: result)
 | 
						|
                                    cell.set(.height, to: .height, of: result)
 | 
						|
                                    
 | 
						|
                                    return result
 | 
						|
                                }
 | 
						|
                            ),
 | 
						|
                            rightAccessory: .toggle(
 | 
						|
                                .boolValue(
 | 
						|
                                    key: .typingIndicatorsEnabled,
 | 
						|
                                    value: current.typingIndicatorsEnabled,
 | 
						|
                                    oldValue: (previous ?? current).typingIndicatorsEnabled
 | 
						|
                                )
 | 
						|
                            ),
 | 
						|
                            onTap: {
 | 
						|
                                Storage.shared.write { db in
 | 
						|
                                    try db.setAndUpdateConfig(.typingIndicatorsEnabled, to: !db[.typingIndicatorsEnabled])
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                        )
 | 
						|
                    ]
 | 
						|
                ),
 | 
						|
                SectionModel(
 | 
						|
                    model: .linkPreviews,
 | 
						|
                    elements: [
 | 
						|
                        SessionCell.Info(
 | 
						|
                            id: .linkPreviews,
 | 
						|
                            title: "PRIVACY_LINK_PREVIEWS_TITLE".localized(),
 | 
						|
                            subtitle: "PRIVACY_LINK_PREVIEWS_DESCRIPTION".localized(),
 | 
						|
                            rightAccessory: .toggle(
 | 
						|
                                .boolValue(
 | 
						|
                                    key: .areLinkPreviewsEnabled,
 | 
						|
                                    value: current.areLinkPreviewsEnabled,
 | 
						|
                                    oldValue: (previous ?? current).areLinkPreviewsEnabled
 | 
						|
                                )
 | 
						|
                            ),
 | 
						|
                            onTap: {
 | 
						|
                                Storage.shared.write { db in
 | 
						|
                                    try db.setAndUpdateConfig(.areLinkPreviewsEnabled, to: !db[.areLinkPreviewsEnabled])
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                        )
 | 
						|
                    ]
 | 
						|
                ),
 | 
						|
                SectionModel(
 | 
						|
                    model: .calls,
 | 
						|
                    elements: [
 | 
						|
                        SessionCell.Info(
 | 
						|
                            id: .calls,
 | 
						|
                            title: "PRIVACY_CALLS_TITLE".localized(),
 | 
						|
                            subtitle: "PRIVACY_CALLS_DESCRIPTION".localized(),
 | 
						|
                            rightAccessory: .toggle(
 | 
						|
                                .boolValue(
 | 
						|
                                    key: .areCallsEnabled,
 | 
						|
                                    value: current.areCallsEnabled,
 | 
						|
                                    oldValue: (previous ?? current).areCallsEnabled
 | 
						|
                                )
 | 
						|
                            ),
 | 
						|
                            accessibility: Accessibility(
 | 
						|
                                label: "Allow voice and video calls"
 | 
						|
                            ),
 | 
						|
                            confirmationInfo: ConfirmationModal.Info(
 | 
						|
                                title: "PRIVACY_CALLS_WARNING_TITLE".localized(),
 | 
						|
                                body: .text("PRIVACY_CALLS_WARNING_DESCRIPTION".localized()),
 | 
						|
                                showCondition: .disabled,
 | 
						|
                                confirmTitle: "continue_2".localized(),
 | 
						|
                                confirmAccessibility: Accessibility(identifier: "Enable"),
 | 
						|
                                confirmStyle: .textPrimary,
 | 
						|
                                onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() }
 | 
						|
                            ),
 | 
						|
                            onTap: {
 | 
						|
                                Storage.shared.write { db in
 | 
						|
                                    try db.setAndUpdateConfig(.areCallsEnabled, to: !db[.areCallsEnabled])
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                        )
 | 
						|
                    ]
 | 
						|
                )
 | 
						|
            ]
 | 
						|
        }
 | 
						|
}
 |