session-ios/Session/Settings/PrivacySettingsViewModel.swift

239 lines
11 KiB
Swift
Raw Normal View History

// 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)),
2023-04-26 07:13:39 +02:00
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(),
2023-04-26 07:13:39 +02:00
confirmAccessibility: Accessibility(identifier: "Enable"),
confirmStyle: .textPrimary,
onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() }
),
onTap: {
Storage.shared.write { db in
db[.areCallsEnabled] = !db[.areCallsEnabled]
}
}
)
]
)
]
}
.removeDuplicates()
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
.handleEvents(didFail: { SNLog("[PrivacySettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self)
}