session-ios/Session/Settings/PrivacySettingsViewModel.swift
Morgan Pretty 742c4a161f Merge remote-tracking branch 'upstream/dev' into feature/updated-user-config-handling
# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	Session/Conversations/ConversationVC+Interaction.swift
#	Session/Conversations/ConversationViewModel.swift
#	Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift
#	Session/Media Viewing & Editing/GIFs/GiphyAPI.swift
#	Session/Meta/Translations/de.lproj/Localizable.strings
#	Session/Meta/Translations/en.lproj/Localizable.strings
#	Session/Meta/Translations/es.lproj/Localizable.strings
#	Session/Meta/Translations/fa.lproj/Localizable.strings
#	Session/Meta/Translations/fi.lproj/Localizable.strings
#	Session/Meta/Translations/fr.lproj/Localizable.strings
#	Session/Meta/Translations/hi.lproj/Localizable.strings
#	Session/Meta/Translations/hr.lproj/Localizable.strings
#	Session/Meta/Translations/id-ID.lproj/Localizable.strings
#	Session/Meta/Translations/it.lproj/Localizable.strings
#	Session/Meta/Translations/ja.lproj/Localizable.strings
#	Session/Meta/Translations/nl.lproj/Localizable.strings
#	Session/Meta/Translations/pl.lproj/Localizable.strings
#	Session/Meta/Translations/pt_BR.lproj/Localizable.strings
#	Session/Meta/Translations/ru.lproj/Localizable.strings
#	Session/Meta/Translations/si.lproj/Localizable.strings
#	Session/Meta/Translations/sk.lproj/Localizable.strings
#	Session/Meta/Translations/sv.lproj/Localizable.strings
#	Session/Meta/Translations/th.lproj/Localizable.strings
#	Session/Meta/Translations/vi-VN.lproj/Localizable.strings
#	Session/Meta/Translations/zh-Hant.lproj/Localizable.strings
#	Session/Meta/Translations/zh_CN.lproj/Localizable.strings
#	Session/Notifications/AppNotifications.swift
#	Session/Notifications/SyncPushTokensJob.swift
#	Session/Notifications/UserNotificationsAdaptee.swift
#	SessionMessagingKit/Configuration.swift
#	SessionMessagingKit/Database/Models/Interaction.swift
#	SessionMessagingKit/Database/Models/SessionThread.swift
#	SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift
#	SessionMessagingKit/Jobs/Types/MessageSendJob.swift
#	SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift
#	SessionMessagingKit/Messages/Message.swift
#	SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ReadReceipts.swift
#	SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift
#	SessionMessagingKit/Sending & Receiving/MessageSender.swift
#	SessionMessagingKit/Shared Models/MentionInfo.swift
2023-02-21 11:11:06 +11:00

237 lines
11 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<PrivacySettingsViewModel.NavButton, PrivacySettingsViewModel.Section, PrivacySettingsViewModel.Item> {
private let shouldShowCloseButton: Bool
// MARK: - Initialization
init(shouldShowCloseButton: Bool = false) {
self.shouldShowCloseButton = shouldShowCloseButton
super.init()
}
// MARK: - Config
enum NavButton: Equatable {
case close
}
public enum Section: SessionTableSection {
case screenSecurity
case readReceipts
case typingIndicators
case linkPreviews
case calls
var title: String? {
switch self {
case .screenSecurity: return "PRIVACY_SECTION_SCREEN_SECURITY".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 Item: Differentiable {
case screenLock
case screenshotNotifications
case readReceipts
case typingIndicators
case linkPreviews
case calls
}
// MARK: - Navigation
override var leftNavItems: AnyPublisher<[NavItem]?, Never> {
guard self.shouldShowCloseButton else { return Just([]).eraseToAnyPublisher() }
return Just([
NavItem(
id: .close,
image: UIImage(named: "X")?
.withRenderingMode(.alwaysTemplate),
style: .plain,
accessibilityIdentifier: "Close Button"
) { [weak self] in
self?.dismissScreen()
}
]).eraseToAnyPublisher()
}
// MARK: - Content
override var title: String { "PRIVACY_TITLE".localized() }
public override var observableTableData: ObservableData { _observableTableData }
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance
///
/// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`)
/// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { db -> [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(.settingBool(key: .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
db[.isScreenLockEnabled] = !db[.isScreenLockEnabled]
}
}
)
]
),
SectionModel(
model: .readReceipts,
elements: [
SessionCell.Info(
id: .readReceipts,
title: "PRIVACY_READ_RECEIPTS_TITLE".localized(),
subtitle: "PRIVACY_READ_RECEIPTS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areReadReceiptsEnabled)),
onTap: {
Storage.shared.write { db in
db[.areReadReceiptsEnabled] = !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(.settingBool(key: .typingIndicatorsEnabled)),
onTap: {
Storage.shared.write { db in
db[.typingIndicatorsEnabled] = !db[.typingIndicatorsEnabled]
}
}
)
]
),
SectionModel(
model: .linkPreviews,
elements: [
SessionCell.Info(
id: .linkPreviews,
title: "PRIVACY_LINK_PREVIEWS_TITLE".localized(),
subtitle: "PRIVACY_LINK_PREVIEWS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areLinkPreviewsEnabled)),
onTap: {
Storage.shared.write { db in
db[.areLinkPreviewsEnabled] = !db[.areLinkPreviewsEnabled]
}
}
)
]
),
SectionModel(
model: .calls,
elements: [
SessionCell.Info(
id: .calls,
title: "PRIVACY_CALLS_TITLE".localized(),
subtitle: "PRIVACY_CALLS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areCallsEnabled)),
accessibility: SessionCell.Accessibility(
label: "Allow voice and video calls"
),
confirmationInfo: ConfirmationModal.Info(
title: "PRIVACY_CALLS_WARNING_TITLE".localized(),
explanation: "PRIVACY_CALLS_WARNING_DESCRIPTION".localized(),
stateToShow: .whenDisabled,
confirmTitle: "continue_2".localized(),
confirmAccessibilityLabel: "Enable",
confirmStyle: .textPrimary,
onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() }
),
onTap: {
Storage.shared.write { db in
db[.areCallsEnabled] = !db[.areCallsEnabled]
}
}
)
]
)
]
}
.removeDuplicates()
.publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self)
}