WIP: add permission chain for voice and video calls

pull/1061/head
Ryan ZHAO 3 months ago
parent c1fcf8e4aa
commit 46f82fd557

@ -13,8 +13,6 @@ import SessionUtilitiesKit
import SessionSnodeKit import SessionSnodeKit
public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
@objc static let isEnabled = true
private let dependencies: Dependencies private let dependencies: Dependencies
// MARK: - Metadata Properties // MARK: - Metadata Properties

@ -88,7 +88,6 @@ extension ConversationVC:
// MARK: - Call // MARK: - Call
@objc func startCall(_ sender: Any?) { @objc func startCall(_ sender: Any?) {
guard SessionCall.isEnabled else { return }
guard viewModel.threadData.threadIsBlocked == false else { return } guard viewModel.threadData.threadIsBlocked == false else { return }
guard Storage.shared[.areCallsEnabled] else { guard Storage.shared[.areCallsEnabled] else {
let confirmationModal: ConfirmationModal = ConfirmationModal( let confirmationModal: ConfirmationModal = ConfirmationModal(
@ -102,7 +101,8 @@ extension ConversationVC:
let navController: UINavigationController = StyledNavigationController( let navController: UINavigationController = StyledNavigationController(
rootViewController: SessionTableViewController( rootViewController: SessionTableViewController(
viewModel: PrivacySettingsViewModel( viewModel: PrivacySettingsViewModel(
shouldShowCloseButton: true shouldShowCloseButton: true,
shouldAutomaticallyShowCallModal: true
) )
) )
) )
@ -116,7 +116,39 @@ extension ConversationVC:
return return
} }
Permissions.requestMicrophonePermissionIfNeeded() guard Permissions.microphone == .granted else {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "Permissions Required",
body: .text("Microphone access is required to make calls and record audio messages. Toggle the \"Microphone\" permission in Settings to continue."),
showCondition: .disabled,
confirmTitle: "sessionSettings".localized(),
onConfirm: { _ in
UIApplication.shared.openSystemSettings()
}
)
)
self.navigationController?.present(confirmationModal, animated: true, completion: nil)
return
}
guard Permissions.localNetwork == .granted else {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "Permissions Required",
body: .text("Local Network access is required to facilitate calls. Toggle the \"Local Network\" permission in Settings to continue."),
showCondition: .disabled,
confirmTitle: "sessionSettings".localized(),
onConfirm: { _ in
UIApplication.shared.openSystemSettings()
}
)
)
self.navigationController?.present(confirmationModal, animated: true, completion: nil)
return
}
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId

@ -1288,7 +1288,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
} }
else { else {
let shouldHaveCallButton: Bool = ( let shouldHaveCallButton: Bool = (
SessionCall.isEnabled &&
(threadData?.threadVariant ?? initialVariant) == .contact && (threadData?.threadVariant ?? initialVariant) == .contact &&
(threadData?.threadIsNoteToSelf ?? initialIsNoteToSelf) == false && (threadData?.threadIsNoteToSelf ?? initialIsNoteToSelf) == false &&
(threadData?.threadIsBlocked ?? initialIsBlocked) == false (threadData?.threadIsBlocked ?? initialIsBlocked) == false

@ -261,7 +261,6 @@ final class ConversationTitleView: UIView {
// Contact threads also have the call button to compensate for // Contact threads also have the call button to compensate for
let shouldShowCallButton: Bool = ( let shouldShowCallButton: Bool = (
SessionCall.isEnabled &&
!isNoteToSelf && !isNoteToSelf &&
threadVariant == .contact threadVariant == .contact
) )

@ -269,6 +269,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
guard Singleton.hasAppContext else { return } guard Singleton.hasAppContext else { return }
Singleton.appContext.clearOldTemporaryDirectories() Singleton.appContext.clearOldTemporaryDirectories()
if Storage.shared[.areCallsEnabled] {
Permissions.checkLocalNetworkPermission()
}
} }
func applicationWillResignActive(_ application: UIApplication) { func applicationWillResignActive(_ application: UIApplication) {

File diff suppressed because one or more lines are too long

@ -16,12 +16,14 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
public let state: TableDataState<Section, TableItem> = TableDataState() public let state: TableDataState<Section, TableItem> = TableDataState()
public let observableState: ObservableTableSourceState<Section, TableItem> = ObservableTableSourceState() public let observableState: ObservableTableSourceState<Section, TableItem> = ObservableTableSourceState()
private let shouldShowCloseButton: Bool private let shouldShowCloseButton: Bool
private let shouldAutomaticallyShowCallModal: Bool
// MARK: - Initialization // MARK: - Initialization
init(shouldShowCloseButton: Bool = false, using dependencies: Dependencies = Dependencies()) { init(shouldShowCloseButton: Bool = false, shouldAutomaticallyShowCallModal: Bool = false, using dependencies: Dependencies = Dependencies()) {
self.dependencies = dependencies self.dependencies = dependencies
self.shouldShowCloseButton = shouldShowCloseButton self.shouldShowCloseButton = shouldShowCloseButton
self.shouldAutomaticallyShowCallModal = shouldAutomaticallyShowCallModal
} }
// MARK: - Config // MARK: - Config
@ -53,13 +55,16 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
} }
public enum TableItem: Differentiable { public enum TableItem: Differentiable {
case calls
case microphone
case camera
case localNetwork
case screenLock case screenLock
case communityMessageRequests case communityMessageRequests
case screenshotNotifications case screenshotNotifications
case readReceipts case readReceipts
case typingIndicators case typingIndicators
case linkPreviews case linkPreviews
case calls
} }
// MARK: - Navigation // MARK: - Navigation
@ -102,6 +107,108 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
} }
.mapWithPrevious { [dependencies] previous, current -> [SectionModel] in .mapWithPrevious { [dependencies] previous, current -> [SectionModel] in
return [ return [
SectionModel(
model: .calls,
elements: [
SessionCell.Info(
id: .calls,
title: "callsVoiceAndVideo".localized(),
subtitle: "callsVoiceAndVideoToggleDescription".localized(),
rightAccessory: .toggle(
.boolValue(
key: .areCallsEnabled,
value: current.areCallsEnabled,
oldValue: (previous ?? current).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".localized()),
showCondition: .disabled,
confirmTitle: "theContinue".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
onConfirm: { _ in
Permissions.requestMicrophonePermissionIfNeeded()
Permissions.requestCameraPermissionIfNeeded()
Permissions.requestLocalNetworkPermissionIfNeeded()
}
),
onTap: {
Storage.shared.write { db in
try db.setAndUpdateConfig(
.areCallsEnabled,
to: !db[.areCallsEnabled],
using: dependencies
)
}
}
)
].appending(
contentsOf: (
!current.areCallsEnabled ? nil :
[
SessionCell.Info(
id: .microphone,
title: "permissionsMicrophone".localized(),
subtitle: "Allow access to microphone for voice calls and audio messages",
rightAccessory: .toggle(
.staticBoolValue((Permissions.microphone == .granted)),
accessibility: Accessibility(
identifier: "Microphone Permission - Switch"
)
),
accessibility: Accessibility(
label: "Grant microphone permission"
),
onTap: {
UIApplication.shared.openSystemSettings()
}
),
SessionCell.Info(
id: .camera,
title: "contentDescriptionCamera".localized(),
subtitle: "Allow access to camera for video calls",
rightAccessory: .toggle(
.staticBoolValue((Permissions.camera == .granted)),
accessibility: Accessibility(
identifier: "Camera Permission - Switch"
)
),
accessibility: Accessibility(
label: "Grant camera permission"
),
onTap: {
UIApplication.shared.openSystemSettings()
}
),
SessionCell.Info(
id: .localNetwork,
title: "Local Network",
subtitle: "Allow access to local network to facilitate voice and video calls",
rightAccessory: .toggle(
.staticBoolValue((Permissions.localNetwork == .granted)),
accessibility: Accessibility(
identifier: "Local Network Permission - Switch"
)
),
accessibility: Accessibility(
label: "Grant local network permission"
),
onTap: {
UIApplication.shared.openSystemSettings()
}
)
]
)
)
),
SectionModel( SectionModel(
model: .screenSecurity, model: .screenSecurity,
elements: [ elements: [
@ -299,48 +406,35 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
} }
) )
] ]
),
SectionModel(
model: .calls,
elements: [
SessionCell.Info(
id: .calls,
title: "callsVoiceAndVideo".localized(),
subtitle: "callsVoiceAndVideoToggleDescription".localized(),
rightAccessory: .toggle(
.boolValue(
key: .areCallsEnabled,
value: current.areCallsEnabled,
oldValue: (previous ?? current).areCallsEnabled
),
accessibility: Accessibility(
identifier: "Voice and Video Calls - Switch"
) )
), ]
accessibility: Accessibility( }
label: "Allow voice and video calls"
), func onAppear(targetViewController: BaseVC) {
confirmationInfo: ConfirmationModal.Info( if self.shouldAutomaticallyShowCallModal {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "callsVoiceAndVideoBeta".localized(), title: "callsVoiceAndVideoBeta".localized(),
body: .text("callsVoiceAndVideoModalDescription".localized()), body: .text("callsVoiceAndVideoModalDescription".localized()),
showCondition: .disabled, showCondition: .disabled,
confirmTitle: "theContinue".localized(), confirmTitle: "theContinue".localized(),
confirmStyle: .danger, confirmStyle: .danger,
cancelStyle: .alert_text, cancelStyle: .alert_text,
onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() } onConfirm: { _ in
), Permissions.requestMicrophonePermissionIfNeeded()
onTap: { Permissions.requestCameraPermissionIfNeeded()
Permissions.requestLocalNetworkPermissionIfNeeded()
Storage.shared.write { db in Storage.shared.write { db in
try db.setAndUpdateConfig( try db.setAndUpdateConfig(
.areCallsEnabled, .areCallsEnabled,
to: !db[.areCallsEnabled], to: !db[.areCallsEnabled],
using: dependencies using: self.dependencies
) )
} }
} }
) )
]
) )
] targetViewController.present(confirmationModal, animated: true, completion: nil)
}
} }
} }

@ -163,6 +163,7 @@ class SessionTableViewController<ViewModel>: BaseVC, UITableViewDataSource, UITa
viewHasAppeared = true viewHasAppeared = true
autoLoadNextPageIfNeeded() autoLoadNextPageIfNeeded()
viewModel.onAppear(targetViewController: self)
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
@ -583,7 +584,12 @@ class SessionTableViewController<ViewModel>: BaseVC, UITableViewDataSource, UITa
let confirmationModal: ConfirmationModal = ConfirmationModal( let confirmationModal: ConfirmationModal = ConfirmationModal(
targetView: tappedView, targetView: tappedView,
info: confirmationInfo info: confirmationInfo
.with(onConfirm: { _ in performAction() }) .with(
onConfirm: { modal in
confirmationInfo.onConfirm?(modal)
performAction()
}
)
) )
present(confirmationModal, animated: true, completion: nil) present(confirmationModal, animated: true, completion: nil)
} }

@ -25,6 +25,7 @@ protocol SessionTableViewModel: AnyObject, SectionedTableData {
func canEditRow(at indexPath: IndexPath) -> Bool func canEditRow(at indexPath: IndexPath) -> Bool
func leadingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration? func leadingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration?
func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration? func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration?
func onAppear(targetViewController: BaseVC)
} }
extension SessionTableViewModel { extension SessionTableViewModel {
@ -43,6 +44,7 @@ extension SessionTableViewModel {
func canEditRow(at indexPath: IndexPath) -> Bool { false } func canEditRow(at indexPath: IndexPath) -> Bool { false }
func leadingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration? { nil } func leadingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration? { nil }
func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration? { nil } func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration? { nil }
func onAppear(targetViewController: BaseVC) { }
} }
// MARK: - SessionTableViewCellType // MARK: - SessionTableViewCellType

@ -415,6 +415,7 @@ extension SessionCell.Accessory {
public enum DataSource: Hashable, Equatable { public enum DataSource: Hashable, Equatable {
case boolValue(key: String, value: Bool, oldValue: Bool) case boolValue(key: String, value: Bool, oldValue: Bool)
case dynamicString(() -> String?) case dynamicString(() -> String?)
case staticBoolValue(Bool)
static func boolValue(_ value: Bool, oldValue: Bool) -> DataSource { static func boolValue(_ value: Bool, oldValue: Bool) -> DataSource {
return .boolValue(key: "", value: value, oldValue: oldValue) return .boolValue(key: "", value: value, oldValue: oldValue)
@ -430,12 +431,14 @@ extension SessionCell.Accessory {
switch self { switch self {
case .boolValue(_, let value, _): return value case .boolValue(_, let value, _): return value
case .dynamicString: return false case .dynamicString: return false
case .staticBoolValue(let value): return value
} }
} }
public var oldBoolValue: Bool { public var oldBoolValue: Bool {
switch self { switch self {
case .boolValue(_, _, let oldValue): return oldValue case .boolValue(_, _, let oldValue): return oldValue
case .staticBoolValue(let value): return value
default: return false default: return false
} }
} }
@ -457,6 +460,7 @@ extension SessionCell.Accessory {
oldValue.hash(into: &hasher) oldValue.hash(into: &hasher)
case .dynamicString(let generator): generator().hash(into: &hasher) case .dynamicString(let generator): generator().hash(into: &hasher)
case .staticBoolValue(let value): value.hash(into: &hasher)
} }
} }
@ -472,6 +476,9 @@ extension SessionCell.Accessory {
case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)): case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)):
return (lhsGenerator() == rhsGenerator()) return (lhsGenerator() == rhsGenerator())
case (.staticBoolValue(let lhsValue), .staticBoolValue(let rhsValue)):
return (lhsValue == rhsValue)
default: return false default: return false
} }
} }

@ -7,6 +7,7 @@ import AVFAudio
import SessionUIKit import SessionUIKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionMessagingKit import SessionMessagingKit
import Network
extension Permissions { extension Permissions {
@discardableResult public static func requestCameraPermissionIfNeeded( @discardableResult public static func requestCameraPermissionIfNeeded(
@ -165,4 +166,31 @@ extension Permissions {
default: return default: return
} }
} }
public static func requestLocalNetworkPermissionIfNeeded() {
checkLocalNetworkPermission()
}
public static func checkLocalNetworkPermission() {
let connection = NWConnection(host: "192.168.1.1", port: 80, using: .tcp)
connection.stateUpdateHandler = { newState in
switch newState {
case .ready:
UserDefaults.sharedLokiProject?[.lastSeenHasLocalNetworkPermission] = true
connection.cancel() // Stop connection since we only need permission status
case .failed(let error):
switch error {
case .posix(let code):
if code.rawValue == 13 {
UserDefaults.sharedLokiProject?[.lastSeenHasLocalNetworkPermission] = false
}
default:
break
}
default:
break
}
}
connection.start(queue: .main)
}
} }

@ -426,7 +426,7 @@ public extension ConfirmationModal {
let hasCloseButton: Bool let hasCloseButton: Bool
let dismissOnConfirm: Bool let dismissOnConfirm: Bool
let dismissType: Modal.DismissType let dismissType: Modal.DismissType
let onConfirm: ((ConfirmationModal) -> ())? public let onConfirm: ((ConfirmationModal) -> ())?
let onCancel: ((ConfirmationModal) -> ())? let onCancel: ((ConfirmationModal) -> ())?
let afterClosed: (() -> ())? let afterClosed: (() -> ())?

@ -35,6 +35,7 @@ public enum SNUserDefaults {
case isMainAppActive case isMainAppActive
case isCallOngoing case isCallOngoing
case lastSeenHasMicrophonePermission case lastSeenHasMicrophonePermission
case lastSeenHasLocalNetworkPermission
} }
public enum Date: Swift.String { public enum Date: Swift.String {

@ -1,17 +1,18 @@
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
import AVFAudio import AVFAudio
import AVFoundation
public enum Permissions { public enum Permissions {
public enum MicrophonePermisson { public enum Status {
case denied case denied
case granted case granted
case undetermined case undetermined
case unknown case unknown
} }
public static var microphone: MicrophonePermisson { public static var microphone: Status {
if #available(iOSApplicationExtension 17.0, *) { if #available(iOSApplicationExtension 17.0, *) {
switch AVAudioApplication.shared.recordPermission { switch AVAudioApplication.shared.recordPermission {
case .undetermined: case .undetermined:
@ -36,4 +37,27 @@ public enum Permissions {
} }
} }
} }
public static var camera: Status {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .notDetermined:
return .undetermined
case .restricted, .denied:
return .denied
case .authorized:
return .granted
@unknown default:
return .unknown
}
}
public static var localNetwork: Status {
let status: Bool? = UserDefaults.sharedLokiProject?[.lastSeenHasLocalNetworkPermission]
switch status {
case .none:
return .unknown
case .some(let value):
return value ? .granted : .denied
}
}
} }

Loading…
Cancel
Save