mirror of https://github.com/oxen-io/session-ios
Browse Source
# Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Closed Groups/EditClosedGroupVC.swift # Session/Closed Groups/NewClosedGroupVC.swift # Session/Conversations/Context Menu/ContextMenuVC+Action.swift # Session/Conversations/Context Menu/ContextMenuVC.swift # Session/Conversations/ConversationMessageMapping.swift # Session/Conversations/ConversationSearch.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewItem.h # Session/Conversations/ConversationViewItem.m # Session/Conversations/ConversationViewModel.m # Session/Conversations/Input View/InputView.swift # Session/Conversations/Input View/MentionSelectionView.swift # Session/Conversations/LongTextViewController.swift # Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift # Session/Conversations/Message Cells/MessageCell.swift # Session/Conversations/Message Cells/VisibleMessageCell.swift # Session/Conversations/Settings/OWSConversationSettingsViewController.m # Session/Conversations/Views & Modals/ConversationTitleView.swift # Session/Conversations/Views & Modals/DownloadAttachmentModal.swift # Session/Conversations/Views & Modals/JoinOpenGroupModal.swift # Session/Conversations/Views & Modals/LinkPreviewModal.swift # Session/Conversations/Views & Modals/MessagesTableView.swift # Session/Conversations/Views & Modals/URLModal.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/Message Requests/MessageRequestsViewController.swift # Session/Media Viewing & Editing/MediaDetailViewController.m # Session/Media Viewing & Editing/MediaPageViewController.swift # Session/Meta/AppDelegate.m # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/Signal-Bridging-Header.h # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Notifications/AppNotifications.swift # Session/Open Groups/JoinOpenGroupVC.swift # Session/Settings/NukeDataModal.swift # Session/Settings/SeedModal.swift # Session/Settings/SettingsVC.swift # Session/Settings/ShareLogsModal.swift # Session/Shared/ConversationCell.swift # Session/Shared/UserSelectionVC.swift # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MentionUtilities.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Database/OWSPrimaryStorage.m # SessionMessagingKit/Database/SSKPreferences.swift # SessionMessagingKit/Database/Storage+Contacts.swift # SessionMessagingKit/Database/Storage+Jobs.swift # SessionMessagingKit/Database/Storage+Messaging.swift # SessionMessagingKit/Database/Storage+OpenGroups.swift # SessionMessagingKit/Database/TSDatabaseView.m # SessionMessagingKit/File Server/FileServerAPIV2.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/AttachmentUploadJob.swift # SessionMessagingKit/Jobs/JobQueue.swift # SessionMessagingKit/Jobs/MessageReceiveJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/NotifyPNServerJob.swift # SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift # SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift # SessionMessagingKit/Messages/Message+Destination.swift # SessionMessagingKit/Messages/Signal/TSIncomingMessage.h # SessionMessagingKit/Messages/Signal/TSIncomingMessage.m # SessionMessagingKit/Messages/Signal/TSInfoMessage.h # SessionMessagingKit/Messages/Signal/TSInfoMessage.m # SessionMessagingKit/Messages/Signal/TSInteraction.h # SessionMessagingKit/Messages/Signal/TSInteraction.m # SessionMessagingKit/Messages/Signal/TSMessage.h # SessionMessagingKit/Messages/Signal/TSMessage.m # SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift # SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift # SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift # SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift # SessionMessagingKit/Sending & Receiving/Mentions/MentionsManager.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h # SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Storage.swift # SessionMessagingKit/Threads/Notification+Thread.swift # SessionMessagingKit/Threads/TSContactThread.h # SessionMessagingKit/Threads/TSContactThread.m # SessionMessagingKit/Threads/TSGroupModel.h # SessionMessagingKit/Threads/TSGroupModel.m # SessionMessagingKit/Threads/TSGroupThread.m # SessionMessagingKit/Utilities/General.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionSnodeKit/OnionRequestAPI+Encryption.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionSnodeKit/SnodeAPI.swift # SessionSnodeKit/SnodeMessage.swift # SessionSnodeKit/Storage+SnodeAPI.swift # SessionSnodeKit/Storage.swift # SessionUtilitiesKit/General/Array+Utilities.swift # SessionUtilitiesKit/General/Dictionary+Utilities.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/Set+Utilities.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Utilities/Optional+Utilities.swift # SessionUtilitiesKit/Utilities/Sodium+Conversion.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift # SignalUtilitiesKit/Messaging/FullTextSearcher.swift # SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift # SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift # SignalUtilitiesKit/To Do/OWSProfileManager.m # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # SignalUtilitiesKit/Utilities/UIView+OWS.swiftpull/612/head
330 changed files with 27806 additions and 5841 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<Scheme |
||||
LastUpgradeVersion = "1320" |
||||
version = "1.3"> |
||||
<BuildAction |
||||
parallelizeBuildables = "YES" |
||||
buildImplicitDependencies = "YES"> |
||||
<BuildActionEntries> |
||||
<BuildActionEntry |
||||
buildForTesting = "YES" |
||||
buildForRunning = "YES" |
||||
buildForProfiling = "YES" |
||||
buildForArchiving = "YES" |
||||
buildForAnalyzing = "YES"> |
||||
<BuildableReference |
||||
BuildableIdentifier = "primary" |
||||
BlueprintIdentifier = "C3C2A6EF25539DE700C340D1" |
||||
BuildableName = "SessionMessagingKit.framework" |
||||
BlueprintName = "SessionMessagingKit" |
||||
ReferencedContainer = "container:Session.xcodeproj"> |
||||
</BuildableReference> |
||||
</BuildActionEntry> |
||||
</BuildActionEntries> |
||||
</BuildAction> |
||||
<TestAction |
||||
buildConfiguration = "Debug" |
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
||||
shouldUseLaunchSchemeArgsEnv = "YES" |
||||
codeCoverageEnabled = "YES" |
||||
onlyGenerateCoverageForSpecifiedTargets = "YES"> |
||||
<CodeCoverageTargets> |
||||
<BuildableReference |
||||
BuildableIdentifier = "primary" |
||||
BlueprintIdentifier = "C3C2A6EF25539DE700C340D1" |
||||
BuildableName = "SessionMessagingKit.framework" |
||||
BlueprintName = "SessionMessagingKit" |
||||
ReferencedContainer = "container:Session.xcodeproj"> |
||||
</BuildableReference> |
||||
</CodeCoverageTargets> |
||||
<Testables> |
||||
<TestableReference |
||||
skipped = "NO" |
||||
parallelizable = "YES" |
||||
testExecutionOrdering = "random"> |
||||
<BuildableReference |
||||
BuildableIdentifier = "primary" |
||||
BlueprintIdentifier = "FDC4388D27B9FFC700C60D73" |
||||
BuildableName = "SessionMessagingKitTests.xctest" |
||||
BlueprintName = "SessionMessagingKitTests" |
||||
ReferencedContainer = "container:Session.xcodeproj"> |
||||
</BuildableReference> |
||||
</TestableReference> |
||||
</Testables> |
||||
</TestAction> |
||||
<LaunchAction |
||||
buildConfiguration = "Debug" |
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
||||
launchStyle = "0" |
||||
useCustomWorkingDirectory = "NO" |
||||
ignoresPersistentStateOnLaunch = "NO" |
||||
debugDocumentVersioning = "YES" |
||||
debugServiceExtension = "internal" |
||||
allowLocationSimulation = "YES"> |
||||
</LaunchAction> |
||||
<ProfileAction |
||||
buildConfiguration = "App Store Release" |
||||
shouldUseLaunchSchemeArgsEnv = "YES" |
||||
savedToolIdentifier = "" |
||||
useCustomWorkingDirectory = "NO" |
||||
debugDocumentVersioning = "YES"> |
||||
<MacroExpansion> |
||||
<BuildableReference |
||||
BuildableIdentifier = "primary" |
||||
BlueprintIdentifier = "C3C2A6EF25539DE700C340D1" |
||||
BuildableName = "SessionMessagingKit.framework" |
||||
BlueprintName = "SessionMessagingKit" |
||||
ReferencedContainer = "container:Session.xcodeproj"> |
||||
</BuildableReference> |
||||
</MacroExpansion> |
||||
</ProfileAction> |
||||
<AnalyzeAction |
||||
buildConfiguration = "Debug"> |
||||
</AnalyzeAction> |
||||
<ArchiveAction |
||||
buildConfiguration = "App Store Release" |
||||
revealArchiveInOrganizer = "YES"> |
||||
</ArchiveAction> |
||||
</Scheme> |
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<Scheme |
||||
LastUpgradeVersion = "1320" |
||||
version = "1.3"> |
||||
<BuildAction |
||||
parallelizeBuildables = "YES" |
||||
buildImplicitDependencies = "YES"> |
||||
<BuildActionEntries> |
||||
<BuildActionEntry |
||||
buildForTesting = "YES" |
||||
buildForRunning = "YES" |
||||
buildForProfiling = "YES" |
||||
buildForArchiving = "YES" |
||||
buildForAnalyzing = "YES"> |
||||
<BuildableReference |
||||
BuildableIdentifier = "primary" |
||||
BlueprintIdentifier = "C3C2A678255388CC00C340D1" |
||||
BuildableName = "SessionUtilitiesKit.framework" |
||||
BlueprintName = "SessionUtilitiesKit" |
||||
ReferencedContainer = "container:Session.xcodeproj"> |
||||
</BuildableReference> |
||||
</BuildActionEntry> |
||||
</BuildActionEntries> |
||||
</BuildAction> |
||||
<TestAction |
||||
buildConfiguration = "Debug" |
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
||||
shouldUseLaunchSchemeArgsEnv = "YES" |
||||
codeCoverageEnabled = "YES" |
||||
onlyGenerateCoverageForSpecifiedTargets = "YES"> |
||||
<CodeCoverageTargets> |
||||
<BuildableReference |
||||
BuildableIdentifier = "primary" |
||||
BlueprintIdentifier = "C3C2A678255388CC00C340D1" |
||||
BuildableName = "SessionUtilitiesKit.framework" |
||||
BlueprintName = "SessionUtilitiesKit" |
||||
ReferencedContainer = "container:Session.xcodeproj"> |
||||
</BuildableReference> |
||||
</CodeCoverageTargets> |
||||
<Testables> |
||||
<TestableReference |
||||
skipped = "NO" |
||||
parallelizable = "YES" |
||||
testExecutionOrdering = "random"> |
||||
<BuildableReference |
||||
BuildableIdentifier = "primary" |
||||
BlueprintIdentifier = "FD83B9AE27CF200A005E1583" |
||||
BuildableName = "SessionUtilitiesKitTests.xctest" |
||||
BlueprintName = "SessionUtilitiesKitTests" |
||||
ReferencedContainer = "container:Session.xcodeproj"> |
||||
</BuildableReference> |
||||
</TestableReference> |
||||
</Testables> |
||||
</TestAction> |
||||
<LaunchAction |
||||
buildConfiguration = "Debug" |
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
||||
launchStyle = "0" |
||||
useCustomWorkingDirectory = "NO" |
||||
ignoresPersistentStateOnLaunch = "NO" |
||||
debugDocumentVersioning = "YES" |
||||
debugServiceExtension = "internal" |
||||
allowLocationSimulation = "YES"> |
||||
</LaunchAction> |
||||
<ProfileAction |
||||
buildConfiguration = "App Store Release" |
||||
shouldUseLaunchSchemeArgsEnv = "YES" |
||||
savedToolIdentifier = "" |
||||
useCustomWorkingDirectory = "NO" |
||||
debugDocumentVersioning = "YES"> |
||||
<MacroExpansion> |
||||
<BuildableReference |
||||
BuildableIdentifier = "primary" |
||||
BlueprintIdentifier = "C3C2A678255388CC00C340D1" |
||||
BuildableName = "SessionUtilitiesKit.framework" |
||||
BlueprintName = "SessionUtilitiesKit" |
||||
ReferencedContainer = "container:Session.xcodeproj"> |
||||
</BuildableReference> |
||||
</MacroExpansion> |
||||
</ProfileAction> |
||||
<AnalyzeAction |
||||
buildConfiguration = "Debug"> |
||||
</AnalyzeAction> |
||||
<ArchiveAction |
||||
buildConfiguration = "App Store Release" |
||||
revealArchiveInOrganizer = "YES"> |
||||
</ArchiveAction> |
||||
</Scheme> |
@ -0,0 +1,213 @@
|
||||
// |
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved. |
||||
// |
||||
|
||||
#import "OWSBackupSettingsViewController.h" |
||||
#import "OWSBackup.h" |
||||
#import "Session-Swift.h" |
||||
|
||||
#import <PromiseKit/AnyPromise.h> |
||||
#import <SessionMessagingKit/Environment.h> |
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h> |
||||
#import <SignalUtilitiesKit/UIColor+OWS.h> |
||||
#import <SignalUtilitiesKit/UIFont+OWS.h> |
||||
#import <SessionUtilitiesKit/UIView+OWS.h> |
||||
#import <SessionUtilitiesKit/MIMETypeUtil.h> |
||||
|
||||
NS_ASSUME_NONNULL_BEGIN |
||||
|
||||
@interface OWSBackupSettingsViewController () |
||||
|
||||
@property (nonatomic, nullable) NSError *iCloudError; |
||||
|
||||
@end |
||||
|
||||
#pragma mark - |
||||
|
||||
@implementation OWSBackupSettingsViewController |
||||
|
||||
#pragma mark - Dependencies |
||||
|
||||
- (OWSBackup *)backup |
||||
{ |
||||
OWSAssertDebug(AppEnvironment.shared.backup); |
||||
|
||||
return AppEnvironment.shared.backup; |
||||
} |
||||
|
||||
#pragma mark - |
||||
|
||||
- (void)viewDidLoad |
||||
{ |
||||
[super viewDidLoad]; |
||||
|
||||
self.title = NSLocalizedString(@"SETTINGS_BACKUP", @"Label for the backup view in app settings."); |
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self |
||||
selector:@selector(backupStateDidChange:) |
||||
name:NSNotificationNameBackupStateDidChange |
||||
object:nil]; |
||||
[[NSNotificationCenter defaultCenter] addObserver:self |
||||
selector:@selector(applicationDidBecomeActive:) |
||||
name:OWSApplicationDidBecomeActiveNotification |
||||
object:nil]; |
||||
|
||||
[self updateTableContents]; |
||||
} |
||||
|
||||
- (void)dealloc |
||||
{ |
||||
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
||||
} |
||||
|
||||
- (void)viewDidAppear:(BOOL)animated |
||||
{ |
||||
[super viewDidAppear:animated]; |
||||
|
||||
[self updateTableContents]; |
||||
[self updateICloudStatus]; |
||||
} |
||||
|
||||
- (void)updateICloudStatus |
||||
{ |
||||
__weak OWSBackupSettingsViewController *weakSelf = self; |
||||
[[self.backup ensureCloudKitAccess] |
||||
.then(^{ |
||||
OWSAssertIsOnMainThread(); |
||||
|
||||
weakSelf.iCloudError = nil; |
||||
[weakSelf updateTableContents]; |
||||
}) |
||||
.catch(^(NSError *error) { |
||||
OWSAssertIsOnMainThread(); |
||||
|
||||
weakSelf.iCloudError = error; |
||||
[weakSelf updateTableContents]; |
||||
}) retainUntilComplete]; |
||||
} |
||||
|
||||
#pragma mark - Table Contents |
||||
|
||||
- (void)updateTableContents |
||||
{ |
||||
OWSTableContents *contents = [OWSTableContents new]; |
||||
|
||||
BOOL isBackupEnabled = [OWSBackup.sharedManager isBackupEnabled]; |
||||
|
||||
if (self.iCloudError) { |
||||
OWSTableSection *iCloudSection = [OWSTableSection new]; |
||||
iCloudSection.headerTitle = NSLocalizedString( |
||||
@"SETTINGS_BACKUP_ICLOUD_STATUS", @"Label for iCloud status row in the in the backup settings view."); |
||||
[iCloudSection |
||||
addItem:[OWSTableItem |
||||
longDisclosureItemWithText:[OWSBackupAPI errorMessageForCloudKitAccessError:self.iCloudError] |
||||
actionBlock:^{ |
||||
[[UIApplication sharedApplication] |
||||
openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; |
||||
}]]; |
||||
[contents addSection:iCloudSection]; |
||||
} |
||||
|
||||
// TODO: This UI is temporary. |
||||
// Enabling backup will involve entering and registering a PIN. |
||||
OWSTableSection *enableSection = [OWSTableSection new]; |
||||
enableSection.headerTitle = NSLocalizedString(@"SETTINGS_BACKUP", @"Label for the backup view in app settings."); |
||||
[enableSection |
||||
addItem:[OWSTableItem switchItemWithText: |
||||
NSLocalizedString(@"SETTINGS_BACKUP_ENABLING_SWITCH", |
||||
@"Label for switch in settings that controls whether or not backup is enabled.") |
||||
isOnBlock:^{ |
||||
return [OWSBackup.sharedManager isBackupEnabled]; |
||||
} |
||||
target:self |
||||
selector:@selector(isBackupEnabledDidChange:)]]; |
||||
[contents addSection:enableSection]; |
||||
|
||||
if (isBackupEnabled) { |
||||
// TODO: This UI is temporary. |
||||
// Enabling backup will involve entering and registering a PIN. |
||||
OWSTableSection *progressSection = [OWSTableSection new]; |
||||
[progressSection |
||||
addItem:[OWSTableItem |
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_STATUS", |
||||
@"Label for backup status row in the in the backup settings view.") |
||||
accessoryText:NSStringForBackupExportState(OWSBackup.sharedManager.backupExportState)]]; |
||||
if (OWSBackup.sharedManager.backupExportState == OWSBackupState_InProgress) { |
||||
if (OWSBackup.sharedManager.backupExportDescription) { |
||||
[progressSection |
||||
addItem:[OWSTableItem |
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_PHASE", |
||||
@"Label for phase row in the in the backup settings view.") |
||||
accessoryText:OWSBackup.sharedManager.backupExportDescription]]; |
||||
if (OWSBackup.sharedManager.backupExportProgress) { |
||||
NSUInteger progressPercent |
||||
= (NSUInteger)round(OWSBackup.sharedManager.backupExportProgress.floatValue * 100); |
||||
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; |
||||
[numberFormatter setNumberStyle:NSNumberFormatterPercentStyle]; |
||||
[numberFormatter setMaximumFractionDigits:0]; |
||||
[numberFormatter setMultiplier:@1]; |
||||
NSString *progressString = [numberFormatter stringFromNumber:@(progressPercent)]; |
||||
[progressSection |
||||
addItem:[OWSTableItem |
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_PROGRESS", |
||||
@"Label for phase row in the in the backup settings view.") |
||||
accessoryText:progressString]]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
switch (OWSBackup.sharedManager.backupExportState) { |
||||
case OWSBackupState_Idle: |
||||
case OWSBackupState_Failed: |
||||
case OWSBackupState_Succeeded: |
||||
[progressSection |
||||
addItem:[OWSTableItem disclosureItemWithText: |
||||
NSLocalizedString(@"SETTINGS_BACKUP_BACKUP_NOW", |
||||
@"Label for 'backup now' button in the backup settings view.") |
||||
actionBlock:^{ |
||||
[OWSBackup.sharedManager tryToExportBackup]; |
||||
}]]; |
||||
break; |
||||
case OWSBackupState_InProgress: |
||||
[progressSection |
||||
addItem:[OWSTableItem disclosureItemWithText: |
||||
NSLocalizedString(@"SETTINGS_BACKUP_CANCEL_BACKUP", |
||||
@"Label for 'cancel backup' button in the backup settings view.") |
||||
actionBlock:^{ |
||||
[OWSBackup.sharedManager cancelExportBackup]; |
||||
}]]; |
||||
break; |
||||
} |
||||
|
||||
[contents addSection:progressSection]; |
||||
} |
||||
|
||||
self.contents = contents; |
||||
} |
||||
|
||||
- (void)isBackupEnabledDidChange:(UISwitch *)sender |
||||
{ |
||||
[OWSBackup.sharedManager setIsBackupEnabled:sender.isOn]; |
||||
|
||||
[self updateTableContents]; |
||||
} |
||||
|
||||
#pragma mark - Events |
||||
|
||||
- (void)backupStateDidChange:(NSNotification *)notification |
||||
{ |
||||
OWSAssertIsOnMainThread(); |
||||
|
||||
[self updateTableContents]; |
||||
} |
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification |
||||
{ |
||||
OWSAssertIsOnMainThread(); |
||||
|
||||
[self updateICloudStatus]; |
||||
} |
||||
|
||||
@end |
||||
|
||||
NS_ASSUME_NONNULL_END |
@ -0,0 +1,357 @@
|
||||
import Foundation |
||||
import WebRTC |
||||
import SessionMessagingKit |
||||
import PromiseKit |
||||
import CallKit |
||||
|
||||
public final class SessionCall: NSObject, WebRTCSessionDelegate { |
||||
|
||||
@objc static let isEnabled = true |
||||
|
||||
// MARK: Metadata Properties |
||||
let uuid: String |
||||
let callID: UUID // This is for CallKit |
||||
let sessionID: String |
||||
let mode: Mode |
||||
var audioMode: AudioMode |
||||
let webRTCSession: WebRTCSession |
||||
let isOutgoing: Bool |
||||
var remoteSDP: RTCSessionDescription? = nil |
||||
var callMessageID: String? |
||||
var answerCallAction: CXAnswerCallAction? = nil |
||||
var contactName: String { |
||||
let contact = Storage.shared.getContact(with: self.sessionID) |
||||
return contact?.displayName(for: Contact.Context.regular) ?? "\(self.sessionID.prefix(4))...\(self.sessionID.suffix(4))" |
||||
} |
||||
var profilePicture: UIImage { |
||||
if let result = OWSProfileManager.shared().profileAvatar(forRecipientId: sessionID) { |
||||
return result |
||||
} else { |
||||
return Identicon.generatePlaceholderIcon(seed: sessionID, text: contactName, size: 300) |
||||
} |
||||
} |
||||
|
||||
// MARK: Control |
||||
lazy public var videoCapturer: RTCVideoCapturer = { |
||||
return RTCCameraVideoCapturer(delegate: webRTCSession.localVideoSource) |
||||
}() |
||||
|
||||
var isRemoteVideoEnabled = false { |
||||
didSet { |
||||
remoteVideoStateDidChange?(isRemoteVideoEnabled) |
||||
} |
||||
} |
||||
|
||||
var isMuted = false { |
||||
willSet { |
||||
if newValue { |
||||
webRTCSession.mute() |
||||
} else { |
||||
webRTCSession.unmute() |
||||
} |
||||
} |
||||
} |
||||
var isVideoEnabled = false { |
||||
willSet { |
||||
if newValue { |
||||
webRTCSession.turnOnVideo() |
||||
} else { |
||||
webRTCSession.turnOffVideo() |
||||
} |
||||
} |
||||
} |
||||
|
||||
// MARK: Mode |
||||
enum Mode { |
||||
case offer |
||||
case answer |
||||
} |
||||
|
||||
// MARK: End call mode |
||||
enum EndCallMode { |
||||
case local |
||||
case remote |
||||
case unanswered |
||||
case answeredElsewhere |
||||
} |
||||
|
||||
// MARK: Audio I/O mode |
||||
enum AudioMode { |
||||
case earpiece |
||||
case speaker |
||||
case headphone |
||||
case bluetooth |
||||
} |
||||
|
||||
// MARK: Call State Properties |
||||
var connectingDate: Date? { |
||||
didSet { |
||||
stateDidChange?() |
||||
hasStartedConnectingDidChange?() |
||||
} |
||||
} |
||||
|
||||
var connectedDate: Date? { |
||||
didSet { |
||||
stateDidChange?() |
||||
hasConnectedDidChange?() |
||||
} |
||||
} |
||||
|
||||
var endDate: Date? { |
||||
didSet { |
||||
stateDidChange?() |
||||
hasEndedDidChange?() |
||||
} |
||||
} |
||||
|
||||
// Not yet implemented |
||||
var isOnHold = false { |
||||
didSet { |
||||
stateDidChange?() |
||||
} |
||||
} |
||||
|
||||
// MARK: State Change Callbacks |
||||
var stateDidChange: (() -> Void)? |
||||
var hasStartedConnectingDidChange: (() -> Void)? |
||||
var hasConnectedDidChange: (() -> Void)? |
||||
var hasEndedDidChange: (() -> Void)? |
||||
var remoteVideoStateDidChange: ((Bool) -> Void)? |
||||
var hasStartedReconnecting: (() -> Void)? |
||||
var hasReconnected: (() -> Void)? |
||||
|
||||
// MARK: Derived Properties |
||||
var hasStartedConnecting: Bool { |
||||
get { return connectingDate != nil } |
||||
set { connectingDate = newValue ? Date() : nil } |
||||
} |
||||
|
||||
var hasConnected: Bool { |
||||
get { return connectedDate != nil } |
||||
set { connectedDate = newValue ? Date() : nil } |
||||
} |
||||
|
||||
var hasEnded: Bool { |
||||
get { return endDate != nil } |
||||
set { endDate = newValue ? Date() : nil } |
||||
} |
||||
|
||||
var timeOutTimer: Timer? = nil |
||||
var didTimeout = false |
||||
|
||||
var duration: TimeInterval { |
||||
guard let connectedDate = connectedDate else { |
||||
return 0 |
||||
} |
||||
if let endDate = endDate { |
||||
return endDate.timeIntervalSince(connectedDate) |
||||
} |
||||
|
||||
return Date().timeIntervalSince(connectedDate) |
||||
} |
||||
|
||||
var reconnectTimer: Timer? = nil |
||||
|
||||
// MARK: Initialization |
||||
init(for sessionID: String, uuid: String, mode: Mode, outgoing: Bool = false) { |
||||
self.sessionID = sessionID |
||||
self.uuid = uuid |
||||
self.callID = UUID() |
||||
self.mode = mode |
||||
self.audioMode = .earpiece |
||||
self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: uuid) |
||||
self.isOutgoing = outgoing |
||||
WebRTCSession.current = self.webRTCSession |
||||
super.init() |
||||
self.webRTCSession.delegate = self |
||||
if AppEnvironment.shared.callManager.currentCall == nil { |
||||
AppEnvironment.shared.callManager.currentCall = self |
||||
} else { |
||||
SNLog("[Calls] A call is ongoing.") |
||||
} |
||||
} |
||||
|
||||
func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) { |
||||
guard case .answer = mode else { return } |
||||
setupTimeoutTimer() |
||||
AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in |
||||
completion(error) |
||||
} |
||||
} |
||||
|
||||
func didReceiveRemoteSDP(sdp: RTCSessionDescription) { |
||||
SNLog("[Calls] Did receive remote sdp.") |
||||
remoteSDP = sdp |
||||
if hasStartedConnecting { |
||||
webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally |
||||
} |
||||
} |
||||
|
||||
// MARK: Actions |
||||
func startSessionCall() { |
||||
guard case .offer = mode else { return } |
||||
guard let thread = TSContactThread.fetch(uniqueId: TSContactThread.threadID(fromContactSessionID: sessionID)) else { return } |
||||
|
||||
let message = CallMessage() |
||||
message.sender = getUserHexEncodedPublicKey() |
||||
message.sentTimestamp = NSDate.millisecondTimestamp() |
||||
message.uuid = self.uuid |
||||
message.kind = .preOffer |
||||
let infoMessage = TSInfoMessage.from(message, associatedWith: thread) |
||||
infoMessage.save() |
||||
self.callMessageID = infoMessage.uniqueId |
||||
|
||||
var promise: Promise<Void>! |
||||
Storage.write(with: { transaction in |
||||
promise = self.webRTCSession.sendPreOffer(message, in: thread, using: transaction) |
||||
}, completion: { [weak self] in |
||||
let _ = promise.done { |
||||
Storage.shared.write { transaction in |
||||
self?.webRTCSession.sendOffer(to: self!.sessionID, using: transaction as! YapDatabaseReadWriteTransaction).retainUntilComplete() |
||||
} |
||||
self?.setupTimeoutTimer() |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func answerSessionCall() { |
||||
guard case .answer = mode else { return } |
||||
hasStartedConnecting = true |
||||
if let sdp = remoteSDP { |
||||
webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally |
||||
} |
||||
} |
||||
|
||||
func answerSessionCallInBackground(action: CXAnswerCallAction) { |
||||
answerCallAction = action |
||||
self.answerSessionCall() |
||||
} |
||||
|
||||
func endSessionCall() { |
||||
guard !hasEnded else { return } |
||||
webRTCSession.hangUp() |
||||
Storage.write { transaction in |
||||
self.webRTCSession.endCall(with: self.sessionID, using: transaction) |
||||
} |
||||
hasEnded = true |
||||
} |
||||
|
||||
// MARK: Update call message |
||||
func updateCallMessage(mode: EndCallMode) { |
||||
guard let callMessageID = callMessageID else { return } |
||||
Storage.write { transaction in |
||||
let infoMessage = TSInfoMessage.fetch(uniqueId: callMessageID, transaction: transaction) |
||||
if let messageToUpdate = infoMessage { |
||||
var shouldMarkAsRead = false |
||||
if self.duration > 0 { |
||||
shouldMarkAsRead = true |
||||
} else if self.hasStartedConnecting { |
||||
shouldMarkAsRead = true |
||||
} else { |
||||
switch mode { |
||||
case .local: |
||||
shouldMarkAsRead = true |
||||
fallthrough |
||||
case .remote: |
||||
fallthrough |
||||
case .unanswered: |
||||
if messageToUpdate.callState == .incoming { |
||||
messageToUpdate.updateCallInfoMessage(.missed, using: transaction) |
||||
} |
||||
case .answeredElsewhere: |
||||
shouldMarkAsRead = true |
||||
} |
||||
} |
||||
if shouldMarkAsRead { |
||||
messageToUpdate.markAsRead(atTimestamp: NSDate.ows_millisecondTimeStamp(), trySendReadReceipt: false, transaction: transaction) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// MARK: Renderer |
||||
func attachRemoteVideoRenderer(_ renderer: RTCVideoRenderer) { |
||||
webRTCSession.attachRemoteRenderer(renderer) |
||||
} |
||||
|
||||
func removeRemoteVideoRenderer(_ renderer: RTCVideoRenderer) { |
||||
webRTCSession.removeRemoteRenderer(renderer) |
||||
} |
||||
|
||||
func attachLocalVideoRenderer(_ renderer: RTCVideoRenderer) { |
||||
webRTCSession.attachLocalRenderer(renderer) |
||||
} |
||||
|
||||
// MARK: Delegate |
||||
public func webRTCIsConnected() { |
||||
self.invalidateTimeoutTimer() |
||||
self.reconnectTimer?.invalidate() |
||||
guard !self.hasConnected else { |
||||
hasReconnected?() |
||||
return |
||||
} |
||||
self.hasConnected = true |
||||
self.answerCallAction?.fulfill() |
||||
} |
||||
|
||||
public func isRemoteVideoDidChange(isEnabled: Bool) { |
||||
isRemoteVideoEnabled = isEnabled |
||||
} |
||||
|
||||
public func didReceiveHangUpSignal() { |
||||
self.hasEnded = true |
||||
DispatchQueue.main.async { |
||||
if let currentBanner = IncomingCallBanner.current { currentBanner.dismiss() } |
||||
if let callVC = CurrentAppContext().frontmostViewController() as? CallVC { callVC.handleEndCallMessage() } |
||||
if let miniCallView = MiniCallView.current { miniCallView.dismiss() } |
||||
AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: .remoteEnded) |
||||
} |
||||
} |
||||
|
||||
public func dataChannelDidOpen() { |
||||
// Send initial video status |
||||
if (isVideoEnabled) { |
||||
webRTCSession.turnOnVideo() |
||||
} else { |
||||
webRTCSession.turnOffVideo() |
||||
} |
||||
} |
||||
|
||||
public func reconnectIfNeeded() { |
||||
setupTimeoutTimer() |
||||
hasStartedReconnecting?() |
||||
guard isOutgoing else { return } |
||||
tryToReconnect() |
||||
} |
||||
|
||||
private func tryToReconnect() { |
||||
reconnectTimer?.invalidate() |
||||
if SSKEnvironment.shared.reachabilityManager.isReachable { |
||||
Storage.write { transaction in |
||||
self.webRTCSession.sendOffer(to: self.sessionID, using: transaction, isRestartingICEConnection: true).retainUntilComplete() |
||||
} |
||||
} else { |
||||
reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false) { _ in |
||||
self.tryToReconnect() |
||||
} |
||||
} |
||||
} |
||||
|
||||
// MARK: Timeout |
||||
public func setupTimeoutTimer() { |
||||
invalidateTimeoutTimer() |
||||
let timeInterval: TimeInterval = hasConnected ? 60 : 30 |
||||
timeOutTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: timeInterval, repeats: false) { _ in |
||||
self.didTimeout = true |
||||
AppEnvironment.shared.callManager.endCall(self) { error in |
||||
self.timeOutTimer = nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
public func invalidateTimeoutTimer() { |
||||
timeOutTimer?.invalidate() |
||||
timeOutTimer = nil |
||||
} |
||||
} |
@ -0,0 +1,47 @@
|
||||
extension SessionCallManager { |
||||
@discardableResult |
||||
public func startCallAction() -> Bool { |
||||
guard let call = self.currentCall else { return false } |
||||
call.startSessionCall() |
||||
return true |
||||
} |
||||
|
||||
@discardableResult |
||||
public func answerCallAction() -> Bool { |
||||
guard let call = self.currentCall else { return false } |
||||
if let _ = CurrentAppContext().frontmostViewController() as? CallVC { |
||||
call.answerSessionCall() |
||||
} else { |
||||
guard let presentingVC = CurrentAppContext().frontmostViewController() else { return false } // FIXME: Handle more gracefully |
||||
let callVC = CallVC(for: self.currentCall!) |
||||
if let conversationVC = presentingVC as? ConversationVC { |
||||
callVC.conversationVC = conversationVC |
||||
conversationVC.inputAccessoryView?.isHidden = true |
||||
conversationVC.inputAccessoryView?.alpha = 0 |
||||
} |
||||
presentingVC.present(callVC, animated: true) { |
||||
call.answerSessionCall() |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
@discardableResult |
||||
public func endCallAction() -> Bool { |
||||
guard let call = self.currentCall else { return false } |
||||
call.endSessionCall() |
||||
if call.didTimeout { |
||||
reportCurrentCallEnded(reason: .unanswered) |
||||
} else { |
||||
reportCurrentCallEnded(reason: nil) |
||||
} |
||||
return true |
||||
} |
||||
|
||||
@discardableResult |
||||
public func setMutedCallAction(isMuted: Bool) -> Bool { |
||||
guard let call = self.currentCall else { return false } |
||||
call.isMuted = isMuted |
||||
return true |
||||
} |
||||
} |