mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
760 lines
36 KiB
Swift
760 lines
36 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 state: TableDataState<Section, TableItem> = TableDataState()
|
|
public let observableState: ObservableTableSourceState<Section, TableItem> = ObservableTableSourceState()
|
|
private let shouldShowCloseButton: Bool
|
|
private let shouldAutomaticallyShowCallModal: Bool
|
|
|
|
/// This value is the current state of the view
|
|
@MainActor @Published private(set) var internalState: State
|
|
private var observationTask: Task<Void, Never>?
|
|
|
|
// MARK: - Initialization
|
|
|
|
@MainActor init(
|
|
shouldShowCloseButton: Bool = false,
|
|
shouldAutomaticallyShowCallModal: Bool = false,
|
|
using dependencies: Dependencies
|
|
) {
|
|
self.dependencies = dependencies
|
|
self.shouldShowCloseButton = shouldShowCloseButton
|
|
self.shouldAutomaticallyShowCallModal = shouldAutomaticallyShowCallModal
|
|
self.internalState = State.initialState()
|
|
|
|
observationTask = ObservationBuilder
|
|
.initialValue(self.internalState)
|
|
.debounce(for: .never)
|
|
.using(dependencies: dependencies)
|
|
.query(PrivacySettingsViewModel.queryState)
|
|
.assign { [weak self] updatedState in
|
|
guard let self = self else { return }
|
|
|
|
// FIXME: To slightly reduce the size of the changes this new observation mechanism is currently wired into the old SessionTableViewController observation mechanism, we should refactor it so everything uses the new mechanism
|
|
let oldState: State = self.internalState
|
|
self.internalState = updatedState
|
|
self.pendingTableDataSubject.send(updatedState.sections(viewModel: self, previousState: oldState))
|
|
}
|
|
}
|
|
|
|
// 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 "screenSecurity".localized()
|
|
case .messageRequests: return "sessionMessageRequests".localized()
|
|
case .readReceipts: return "readReceipts".localized()
|
|
case .typingIndicators: return "typingIndicators".localized()
|
|
case .linkPreviews: return "linkPreviews".localized()
|
|
case .calls: return "callsSettings".localized()
|
|
}
|
|
}
|
|
|
|
var style: SessionTableSectionStyle { return .titleRoundedContent }
|
|
}
|
|
|
|
public enum TableItem: Differentiable {
|
|
case calls
|
|
case microphone
|
|
case camera
|
|
case localNetwork
|
|
case screenLock
|
|
case communityMessageRequests
|
|
case screenshotNotifications
|
|
case readReceipts
|
|
case typingIndicators
|
|
case linkPreviews
|
|
}
|
|
|
|
// 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() }
|
|
]
|
|
)
|
|
|
|
public struct PrivacySettingsViewModelEvent: Hashable {
|
|
let isAwaitingCallPermissionChainResult: Bool
|
|
}
|
|
|
|
// MARK: - State
|
|
|
|
public struct State: ObservableKeyProvider {
|
|
let isScreenLockEnabled: Bool
|
|
let checkForCommunityMessageRequests: Bool
|
|
let areReadReceiptsEnabled: Bool
|
|
let typingIndicatorsEnabled: Bool
|
|
let areLinkPreviewsEnabled: Bool
|
|
let areCallsEnabled: Bool
|
|
let cameraPermission: Permissions.Status
|
|
let microphonePermission: Permissions.Status
|
|
let localNetworkPermission: Permissions.Status
|
|
let isAwaitingCallPermissionChainResult: Bool
|
|
|
|
@MainActor public func sections(viewModel: PrivacySettingsViewModel, previousState: State) -> [SectionModel] {
|
|
PrivacySettingsViewModel.sections(
|
|
state: self,
|
|
previousState: previousState,
|
|
viewModel: viewModel
|
|
)
|
|
}
|
|
|
|
public let observedKeys: Set<ObservableKey> = [
|
|
.setting(.isScreenLockEnabled),
|
|
.setting(.checkForCommunityMessageRequests),
|
|
.setting(.areReadReceiptsEnabled),
|
|
.setting(.typingIndicatorsEnabled),
|
|
.setting(.areLinkPreviewsEnabled),
|
|
.setting(.areCallsEnabled),
|
|
.setting(.lastSeenHasLocalNetworkPermission),
|
|
.permission(.microphone),
|
|
.permission(.camera),
|
|
.permission(.localNetwork),
|
|
.updateScreen(PrivacySettingsViewModel.self)
|
|
]
|
|
|
|
static func initialState() -> State {
|
|
return State(
|
|
isScreenLockEnabled: false,
|
|
checkForCommunityMessageRequests: false,
|
|
areReadReceiptsEnabled: false,
|
|
typingIndicatorsEnabled: false,
|
|
areLinkPreviewsEnabled: false,
|
|
areCallsEnabled: false,
|
|
cameraPermission: .unknown,
|
|
microphonePermission: .unknown,
|
|
localNetworkPermission: .unknown,
|
|
isAwaitingCallPermissionChainResult: false
|
|
)
|
|
}
|
|
}
|
|
|
|
let title: String = "sessionPrivacy".localized()
|
|
|
|
@Sendable private static func queryState(
|
|
previousState: State,
|
|
events: [ObservedEvent],
|
|
isInitialQuery: Bool,
|
|
using dependencies: Dependencies
|
|
) async -> State {
|
|
var isScreenLockEnabled: Bool = previousState.isScreenLockEnabled
|
|
var checkForCommunityMessageRequests: Bool = previousState.checkForCommunityMessageRequests
|
|
var areReadReceiptsEnabled: Bool = previousState.areReadReceiptsEnabled
|
|
var typingIndicatorsEnabled: Bool = previousState.typingIndicatorsEnabled
|
|
var areLinkPreviewsEnabled: Bool = previousState.areLinkPreviewsEnabled
|
|
var areCallsEnabled: Bool = previousState.areCallsEnabled
|
|
var cameraPermission: Permissions.Status = previousState.cameraPermission
|
|
var microphonePermission: Permissions.Status = previousState.microphonePermission
|
|
var localNetworkPermission: Permissions.Status = previousState.localNetworkPermission
|
|
var isAwaitingCallPermissionChainResult: Bool = previousState.isAwaitingCallPermissionChainResult
|
|
|
|
/// If we have no previous state then we need to fetch the initial state
|
|
if isInitialQuery {
|
|
dependencies.mutate(cache: .libSession) { libSession in
|
|
isScreenLockEnabled = libSession.get(.isScreenLockEnabled)
|
|
checkForCommunityMessageRequests = libSession.get(.checkForCommunityMessageRequests)
|
|
areReadReceiptsEnabled = libSession.get(.areReadReceiptsEnabled)
|
|
typingIndicatorsEnabled = libSession.get(.typingIndicatorsEnabled)
|
|
areLinkPreviewsEnabled = libSession.get(.areLinkPreviewsEnabled)
|
|
areCallsEnabled = libSession.get(.areCallsEnabled)
|
|
}
|
|
}
|
|
|
|
/// Process any event changes
|
|
events.forEach { event in
|
|
guard let updatedValue: Bool = event.value as? Bool else { return }
|
|
|
|
switch event.key {
|
|
case .setting(.isScreenLockEnabled): isScreenLockEnabled = updatedValue
|
|
case .setting(.checkForCommunityMessageRequests):
|
|
checkForCommunityMessageRequests = updatedValue
|
|
case .setting(.areReadReceiptsEnabled): areReadReceiptsEnabled = updatedValue
|
|
case .setting(.typingIndicatorsEnabled): typingIndicatorsEnabled = updatedValue
|
|
case .setting(.areLinkPreviewsEnabled): areLinkPreviewsEnabled = updatedValue
|
|
case .setting(.areCallsEnabled): areCallsEnabled = updatedValue
|
|
|
|
default: break
|
|
}
|
|
}
|
|
|
|
if
|
|
let event: ObservedEvent = events.first(where: { $0.key == .updateScreen(PrivacySettingsViewModel.self) }),
|
|
let eventValue: PrivacySettingsViewModelEvent = event.value as? PrivacySettingsViewModelEvent
|
|
{
|
|
isAwaitingCallPermissionChainResult = eventValue.isAwaitingCallPermissionChainResult
|
|
}
|
|
|
|
/// Get the proper permissions rather than relying on events
|
|
cameraPermission = Permissions.camera
|
|
microphonePermission = Permissions.microphone
|
|
localNetworkPermission = Permissions.localNetwork(using: dependencies)
|
|
|
|
return State(
|
|
isScreenLockEnabled: isScreenLockEnabled,
|
|
checkForCommunityMessageRequests: checkForCommunityMessageRequests,
|
|
areReadReceiptsEnabled: areReadReceiptsEnabled,
|
|
typingIndicatorsEnabled: typingIndicatorsEnabled,
|
|
areLinkPreviewsEnabled: areLinkPreviewsEnabled,
|
|
areCallsEnabled: areCallsEnabled,
|
|
cameraPermission: cameraPermission,
|
|
microphonePermission: microphonePermission,
|
|
localNetworkPermission: localNetworkPermission,
|
|
isAwaitingCallPermissionChainResult: isAwaitingCallPermissionChainResult
|
|
)
|
|
}
|
|
|
|
private static func sections(
|
|
state: State,
|
|
previousState: State,
|
|
viewModel: PrivacySettingsViewModel
|
|
) -> [SectionModel] {
|
|
var sections: [SectionModel] = []
|
|
|
|
var callsSection = SectionModel(model: .calls, elements: [])
|
|
callsSection.elements.append(
|
|
SessionCell.Info(
|
|
id: .calls,
|
|
title: "callsVoiceAndVideo".localized(),
|
|
subtitle: "callsVoiceAndVideoToggleDescription".localized(),
|
|
trailingAccessory: .toggle(
|
|
state.areCallsEnabled,
|
|
oldValue: previousState.areCallsEnabled,
|
|
accessibility: Accessibility(
|
|
identifier: "Voice and Video Calls - Switch"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
label: "Allow voice and video calls"
|
|
),
|
|
confirmationInfo: ConfirmationModal.Info(
|
|
title: "callsVoiceAndVideoBeta".localized(),
|
|
body: .text("callsVoiceAndVideoModalDescription"
|
|
.put(key: "session_foundation", value: Constants.session_foundation)
|
|
.localized()),
|
|
showCondition: .disabled,
|
|
confirmTitle: "theContinue".localized(),
|
|
confirmStyle: .danger,
|
|
cancelStyle: .alert_text,
|
|
dismissOnConfirm: false,
|
|
onConfirm: { [dependencies = viewModel.dependencies] modal in
|
|
/// Notify that we are awaiting the outcome of the calls permission chain
|
|
dependencies.notifyAsync(
|
|
priority: .immediate,
|
|
key: .updateScreen(PrivacySettingsViewModel.self),
|
|
value: PrivacySettingsViewModelEvent(
|
|
isAwaitingCallPermissionChainResult: true
|
|
)
|
|
)
|
|
|
|
/// Wait until after the modal has been dismissed to kick off the permission chain to ensure we
|
|
/// don't try to present a subsequent modal from the one being dismissed (this would result in
|
|
/// no modal being shown)
|
|
modal.dismiss(animated: true) {
|
|
Permissions.requestPermissionsForCalls(using: dependencies) { _, _, _ in
|
|
/// Notify that we are no longer awaiting the result of the permission chain
|
|
dependencies.notifyAsync(
|
|
priority: .immediate,
|
|
key: .updateScreen(PrivacySettingsViewModel.self),
|
|
value: PrivacySettingsViewModelEvent(
|
|
isAwaitingCallPermissionChainResult: false
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
),
|
|
onTap: { [dependencies = viewModel.dependencies] in
|
|
dependencies.setAsync(.areCallsEnabled, !state.areCallsEnabled)
|
|
}
|
|
)
|
|
)
|
|
|
|
if state.areCallsEnabled {
|
|
let unknownPermissionStatuses: Set<Permissions.Status> = [.undetermined, .unknown]
|
|
|
|
callsSection.elements.append(
|
|
SessionCell.Info(
|
|
id: .microphone,
|
|
title: "permissionsMicrophone".localized(),
|
|
subtitle: "permissionsMicrophoneDescriptionIos".localized(),
|
|
trailingAccessory: {
|
|
guard
|
|
!state.isAwaitingCallPermissionChainResult ||
|
|
!unknownPermissionStatuses.contains(state.microphonePermission)
|
|
else {
|
|
return .activityIndicator(
|
|
themeColor: .textPrimary,
|
|
accessibility: Accessibility(
|
|
identifier: "Microphone Permission"
|
|
)
|
|
)
|
|
}
|
|
|
|
return .toggle(
|
|
(state.microphonePermission == .granted),
|
|
oldValue: (previousState.microphonePermission == .granted),
|
|
accessibility: Accessibility(
|
|
identifier: "Microphone Permission - Switch"
|
|
)
|
|
)
|
|
}(),
|
|
accessibility: Accessibility(
|
|
label: "Grant microphone permission"
|
|
),
|
|
onTap: { [weak viewModel, dependencies = viewModel.dependencies] in
|
|
switch state.microphonePermission {
|
|
case .granted, .restricted:
|
|
viewModel?.transitionToScreen(
|
|
ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "permissionChange".localized(),
|
|
body: .text(
|
|
"permissionsMicrophoneChangeDescriptionIos".localized()
|
|
),
|
|
confirmTitle: "sessionSettings".localized(),
|
|
onConfirm: { _ in
|
|
UIApplication.shared.openSystemSettings()
|
|
}
|
|
)
|
|
),
|
|
transitionType: .present
|
|
)
|
|
return
|
|
|
|
case .denied:
|
|
viewModel?.transitionToScreen(
|
|
ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "permissionsRequired".localized(),
|
|
body: .text(
|
|
"permissionsMicrophoneAccessRequiredCallsIos".localized()
|
|
),
|
|
confirmTitle: "sessionSettings".localized(),
|
|
onConfirm: { _ in
|
|
UIApplication.shared.openSystemSettings()
|
|
}
|
|
)
|
|
),
|
|
transitionType: .present
|
|
)
|
|
return
|
|
|
|
case .undetermined, .unknown:
|
|
Permissions.requestMicrophonePermissionIfNeeded(using: dependencies)
|
|
return
|
|
}
|
|
}
|
|
)
|
|
)
|
|
callsSection.elements.append(
|
|
SessionCell.Info(
|
|
id: .camera,
|
|
title: "contentDescriptionCamera".localized(),
|
|
subtitle: "permissionsCameraDescriptionIos".localized(),
|
|
trailingAccessory: {
|
|
guard
|
|
!state.isAwaitingCallPermissionChainResult ||
|
|
!unknownPermissionStatuses.contains(state.cameraPermission)
|
|
else {
|
|
return .activityIndicator(
|
|
themeColor: .textPrimary,
|
|
accessibility: Accessibility(
|
|
identifier: "Camera Permission"
|
|
)
|
|
)
|
|
}
|
|
|
|
return .toggle(
|
|
(state.cameraPermission == .granted),
|
|
oldValue: (previousState.cameraPermission == .granted),
|
|
accessibility: Accessibility(
|
|
identifier: "Camera Permission - Switch"
|
|
)
|
|
)
|
|
}(),
|
|
accessibility: Accessibility(
|
|
label: "Grant camera permission"
|
|
),
|
|
onTap: { [weak viewModel, dependencies = viewModel.dependencies] in
|
|
switch state.cameraPermission {
|
|
case .granted, .restricted:
|
|
viewModel?.transitionToScreen(
|
|
ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "permissionChange".localized(),
|
|
body: .text(
|
|
"permissionsCameraChangeDescriptionIos".localized()
|
|
),
|
|
confirmTitle: "sessionSettings".localized(),
|
|
onConfirm: { _ in
|
|
UIApplication.shared.openSystemSettings()
|
|
}
|
|
)
|
|
),
|
|
transitionType: .present
|
|
)
|
|
return
|
|
|
|
case .denied:
|
|
viewModel?.transitionToScreen(
|
|
ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "permissionsRequired".localized(),
|
|
body: .text(
|
|
"permissionsCameraAccessRequiredCallsIos".localized()
|
|
),
|
|
confirmTitle: "sessionSettings".localized(),
|
|
onConfirm: { _ in
|
|
UIApplication.shared.openSystemSettings()
|
|
}
|
|
)
|
|
),
|
|
transitionType: .present
|
|
)
|
|
return
|
|
|
|
case .undetermined, .unknown:
|
|
Permissions.requestCameraPermissionIfNeeded(using: dependencies)
|
|
return
|
|
}
|
|
}
|
|
)
|
|
)
|
|
callsSection.elements.append(
|
|
SessionCell.Info(
|
|
id: .localNetwork,
|
|
title: "permissionsLocalNetworkIos".localized(),
|
|
subtitle: "permissionsLocalNetworkDescriptionIos".localized(),
|
|
trailingAccessory: {
|
|
guard
|
|
!state.isAwaitingCallPermissionChainResult ||
|
|
!unknownPermissionStatuses.contains(state.localNetworkPermission)
|
|
else {
|
|
return .activityIndicator(
|
|
themeColor: .textPrimary,
|
|
accessibility: Accessibility(
|
|
identifier: "Local Network Permission"
|
|
)
|
|
)
|
|
}
|
|
|
|
return .toggle(
|
|
(state.localNetworkPermission == .granted),
|
|
oldValue: (previousState.localNetworkPermission == .granted),
|
|
accessibility: Accessibility(
|
|
identifier: "Local Network Permission - Switch"
|
|
)
|
|
)
|
|
}(),
|
|
accessibility: Accessibility(
|
|
label: "Grant local network permission"
|
|
),
|
|
onTap: { [weak viewModel, dependencies = viewModel.dependencies] in
|
|
switch state.localNetworkPermission {
|
|
case .granted, .restricted:
|
|
viewModel?.transitionToScreen(
|
|
ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "permissionChange".localized(),
|
|
body: .text(
|
|
"permissionsLocalNetworkChangeDescriptionIos".localized()
|
|
),
|
|
confirmTitle: "sessionSettings".localized(),
|
|
onConfirm: { _ in
|
|
UIApplication.shared.openSystemSettings()
|
|
}
|
|
)
|
|
),
|
|
transitionType: .present
|
|
)
|
|
return
|
|
|
|
case .denied:
|
|
viewModel?.transitionToScreen(
|
|
ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "permissionsRequired".localized(),
|
|
body: .text(
|
|
"permissionsLocalNetworkAccessRequiredCallsIos".localized()
|
|
),
|
|
confirmTitle: "sessionSettings".localized(),
|
|
onConfirm: { _ in
|
|
UIApplication.shared.openSystemSettings()
|
|
}
|
|
)
|
|
),
|
|
transitionType: .present
|
|
)
|
|
return
|
|
|
|
case .undetermined, .unknown:
|
|
Permissions.requestLocalNetworkPermissionIfNeeded(using: dependencies)
|
|
return
|
|
}
|
|
}
|
|
)
|
|
)
|
|
}
|
|
|
|
sections.append(callsSection)
|
|
sections.append(
|
|
SectionModel(
|
|
model: .screenSecurity,
|
|
elements: [
|
|
SessionCell.Info(
|
|
id: .screenLock,
|
|
title: "lockApp".localized(),
|
|
subtitle: "lockAppDescriptionIos"
|
|
.put(key: "app_name", value: Constants.app_name)
|
|
.localized(),
|
|
trailingAccessory: .toggle(
|
|
state.isScreenLockEnabled,
|
|
oldValue: previousState.isScreenLockEnabled,
|
|
accessibility: Accessibility(
|
|
identifier: "Lock App - Switch"
|
|
)
|
|
),
|
|
onTap: { [weak viewModel, dependencies = viewModel.dependencies] 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 {
|
|
viewModel?.transitionToScreen(
|
|
ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "lockAppEnablePasscode".localized(),
|
|
cancelTitle: "okay".localized(),
|
|
cancelStyle: .alert_text
|
|
)
|
|
),
|
|
transitionType: .present
|
|
)
|
|
return
|
|
}
|
|
|
|
dependencies.setAsync(.isScreenLockEnabled, !state.isScreenLockEnabled)
|
|
}
|
|
)
|
|
]
|
|
)
|
|
)
|
|
sections.append(
|
|
SectionModel(
|
|
model: .messageRequests,
|
|
elements: [
|
|
SessionCell.Info(
|
|
id: .communityMessageRequests,
|
|
title: "messageRequestsCommunities".localized(),
|
|
subtitle: "messageRequestsCommunitiesDescription".localized(),
|
|
trailingAccessory: .toggle(
|
|
state.checkForCommunityMessageRequests,
|
|
oldValue: previousState.checkForCommunityMessageRequests,
|
|
accessibility: Accessibility(
|
|
identifier: "Community Message Requests - Switch"
|
|
)
|
|
),
|
|
onTap: { [dependencies = viewModel.dependencies] in
|
|
dependencies.setAsync(
|
|
.checkForCommunityMessageRequests,
|
|
!state.checkForCommunityMessageRequests
|
|
)
|
|
}
|
|
)
|
|
]
|
|
)
|
|
)
|
|
sections.append(
|
|
SectionModel(
|
|
model: .readReceipts,
|
|
elements: [
|
|
SessionCell.Info(
|
|
id: .readReceipts,
|
|
title: "readReceipts".localized(),
|
|
subtitle: "readReceiptsDescription".localized(),
|
|
trailingAccessory: .toggle(
|
|
state.areReadReceiptsEnabled,
|
|
oldValue: previousState.areReadReceiptsEnabled,
|
|
accessibility: Accessibility(
|
|
identifier: "Read Receipts - Switch"
|
|
)
|
|
),
|
|
onTap: { [dependencies = viewModel.dependencies] in
|
|
dependencies.setAsync(.areReadReceiptsEnabled, !state.areReadReceiptsEnabled)
|
|
}
|
|
)
|
|
]
|
|
)
|
|
)
|
|
sections.append(
|
|
SectionModel(
|
|
model: .typingIndicators,
|
|
elements: [
|
|
SessionCell.Info(
|
|
id: .typingIndicators,
|
|
title: SessionCell.TextInfo(
|
|
"typingIndicators".localized(),
|
|
font: .title
|
|
),
|
|
subtitle: SessionCell.TextInfo(
|
|
"typingIndicatorsDescription".localized(),
|
|
font: .subtitle,
|
|
extraViewGenerator: { TypingIndicatorPreviewView() }
|
|
),
|
|
trailingAccessory: .toggle(
|
|
state.typingIndicatorsEnabled,
|
|
oldValue: previousState.typingIndicatorsEnabled,
|
|
accessibility: Accessibility(
|
|
identifier: "Typing Indicators - Switch"
|
|
)
|
|
),
|
|
onTap: { [dependencies = viewModel.dependencies] in
|
|
dependencies.setAsync(.typingIndicatorsEnabled, !state.typingIndicatorsEnabled)
|
|
}
|
|
)
|
|
]
|
|
)
|
|
)
|
|
sections.append(
|
|
SectionModel(
|
|
model: .linkPreviews,
|
|
elements: [
|
|
SessionCell.Info(
|
|
id: .linkPreviews,
|
|
title: "linkPreviewsSend".localized(),
|
|
subtitle: "linkPreviewsDescription".localized(),
|
|
trailingAccessory: .toggle(
|
|
state.areLinkPreviewsEnabled,
|
|
oldValue: previousState.areLinkPreviewsEnabled,
|
|
accessibility: Accessibility(
|
|
identifier: "Send Link Previews - Switch"
|
|
)
|
|
),
|
|
onTap: { [dependencies = viewModel.dependencies] in
|
|
dependencies.setAsync(.areLinkPreviewsEnabled, !state.areLinkPreviewsEnabled)
|
|
}
|
|
)
|
|
]
|
|
)
|
|
)
|
|
|
|
return sections
|
|
}
|
|
|
|
@MainActor func onAppear(targetViewController: BaseVC) {
|
|
if self.shouldAutomaticallyShowCallModal {
|
|
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
|
info: ConfirmationModal.Info(
|
|
title: "callsVoiceAndVideoBeta".localized(),
|
|
body: .text(
|
|
"callsVoiceAndVideoModalDescription"
|
|
.put(key: "session_foundation", value: Constants.session_foundation)
|
|
.localized()
|
|
),
|
|
showCondition: .disabled,
|
|
confirmTitle: "theContinue".localized(),
|
|
confirmStyle: .danger,
|
|
cancelStyle: .alert_text,
|
|
onConfirm: { [dependencies] _ in
|
|
Permissions.requestPermissionsForCalls(using: dependencies)
|
|
dependencies.setAsync(.areCallsEnabled, true)
|
|
}
|
|
)
|
|
)
|
|
targetViewController.present(confirmationModal, animated: true, completion: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Info
|
|
|
|
private final class TypingIndicatorPreviewView: UIView {
|
|
static var size: CGSize = CGSize(width: 24, height: 14)
|
|
|
|
// MARK: - Components
|
|
|
|
private lazy var bubbleView: UIView = {
|
|
let result: UIView = UIView()
|
|
result.layer.cornerRadius = VisibleMessageCell.smallCornerRadius
|
|
result.layer.mask = bubbleViewMaskLayer
|
|
result.themeBackgroundColor = .messageBubble_incomingBackground
|
|
|
|
return result
|
|
}()
|
|
|
|
private let bubbleViewMaskLayer: CAShapeLayer = {
|
|
let result: CAShapeLayer = CAShapeLayer()
|
|
let maskPath: UIBezierPath = UIBezierPath(
|
|
roundedRect: CGRect(origin: .zero, size: TypingIndicatorPreviewView.size),
|
|
byRoundingCorners: .allCorners,
|
|
cornerRadii: CGSize(
|
|
width: VisibleMessageCell.largeCornerRadius,
|
|
height: VisibleMessageCell.largeCornerRadius
|
|
)
|
|
)
|
|
|
|
result.path = maskPath.cgPath
|
|
|
|
return result
|
|
}()
|
|
public lazy var typingIndicatorView: TypingIndicatorView = TypingIndicatorView()
|
|
|
|
// MARK: - Initialization
|
|
|
|
init() {
|
|
super.init(frame: .zero)
|
|
|
|
setupLayout()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Layout
|
|
|
|
private func setupLayout() {
|
|
addSubview(bubbleView)
|
|
bubbleView.addSubview(typingIndicatorView)
|
|
|
|
set(.width, to: TypingIndicatorPreviewView.size.width)
|
|
set(.height, to: TypingIndicatorPreviewView.size.height)
|
|
|
|
bubbleView.pin(to: self)
|
|
typingIndicatorView.center(in: bubbleView)
|
|
|
|
// Use a transform scale to reduce the size of the typing indicator to the
|
|
// desired size (this way the animation remains intact)
|
|
typingIndicatorView.transform = CGAffineTransform(scaleX: 0.4, y: 0.4)
|
|
typingIndicatorView.startAnimation()
|
|
}
|
|
}
|