WIP: add permission chain for voice and video calls

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

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

@ -88,7 +88,6 @@ extension ConversationVC:
// MARK: - Call
@objc func startCall(_ sender: Any?) {
guard SessionCall.isEnabled else { return }
guard viewModel.threadData.threadIsBlocked == false else { return }
guard Storage.shared[.areCallsEnabled] else {
let confirmationModal: ConfirmationModal = ConfirmationModal(
@ -102,7 +101,8 @@ extension ConversationVC:
let navController: UINavigationController = StyledNavigationController(
rootViewController: SessionTableViewController(
viewModel: PrivacySettingsViewModel(
shouldShowCloseButton: true
shouldShowCloseButton: true,
shouldAutomaticallyShowCallModal: true
)
)
)
@ -116,7 +116,39 @@ extension ConversationVC:
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

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

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

@ -269,6 +269,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
guard Singleton.hasAppContext else { return }
Singleton.appContext.clearOldTemporaryDirectories()
if Storage.shared[.areCallsEnabled] {
Permissions.checkLocalNetworkPermission()
}
}
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 observableState: ObservableTableSourceState<Section, TableItem> = ObservableTableSourceState()
private let shouldShowCloseButton: Bool
private let shouldAutomaticallyShowCallModal: Bool
// 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.shouldShowCloseButton = shouldShowCloseButton
self.shouldAutomaticallyShowCallModal = shouldAutomaticallyShowCallModal
}
// MARK: - Config
@ -53,13 +55,16 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
}
public enum TableItem: Differentiable {
case calls
case microphone
case camera
case localNetwork
case screenLock
case communityMessageRequests
case screenshotNotifications
case readReceipts
case typingIndicators
case linkPreviews
case calls
}
// MARK: - Navigation
@ -102,6 +107,108 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav
}
.mapWithPrevious { [dependencies] previous, current -> [SectionModel] in
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(
model: .screenSecurity,
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"
),
confirmationInfo: ConfirmationModal.Info(
title: "callsVoiceAndVideoBeta".localized(),
body: .text("callsVoiceAndVideoModalDescription".localized()),
showCondition: .disabled,
confirmTitle: "theContinue".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text,
onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() }
),
onTap: {
Storage.shared.write { db in
try db.setAndUpdateConfig(
.areCallsEnabled,
to: !db[.areCallsEnabled],
using: dependencies
)
}
}
)
]
)
]
}
func onAppear(targetViewController: BaseVC) {
if self.shouldAutomaticallyShowCallModal {
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: 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()
Storage.shared.write { db in
try db.setAndUpdateConfig(
.areCallsEnabled,
to: !db[.areCallsEnabled],
using: self.dependencies
)
}
}
)
)
targetViewController.present(confirmationModal, animated: true, completion: nil)
}
}
}

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

@ -25,6 +25,7 @@ protocol SessionTableViewModel: AnyObject, SectionedTableData {
func canEditRow(at indexPath: IndexPath) -> Bool
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 onAppear(targetViewController: BaseVC)
}
extension SessionTableViewModel {
@ -43,6 +44,7 @@ extension SessionTableViewModel {
func canEditRow(at indexPath: IndexPath) -> Bool { false }
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 onAppear(targetViewController: BaseVC) { }
}
// MARK: - SessionTableViewCellType

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

@ -7,6 +7,7 @@ import AVFAudio
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
import Network
extension Permissions {
@discardableResult public static func requestCameraPermissionIfNeeded(
@ -165,4 +166,31 @@ extension Permissions {
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 dismissOnConfirm: Bool
let dismissType: Modal.DismissType
let onConfirm: ((ConfirmationModal) -> ())?
public let onConfirm: ((ConfirmationModal) -> ())?
let onCancel: ((ConfirmationModal) -> ())?
let afterClosed: (() -> ())?

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

@ -1,17 +1,18 @@
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
import AVFAudio
import AVFoundation
public enum Permissions {
public enum MicrophonePermisson {
public enum Status {
case denied
case granted
case undetermined
case unknown
}
public static var microphone: MicrophonePermisson {
public static var microphone: Status {
if #available(iOSApplicationExtension 17.0, *) {
switch AVAudioApplication.shared.recordPermission {
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