diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 96ffbc4f9..b264bf0a8 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -13,6 +13,12 @@ 34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34535D811E256BE9008A4747 /* UIView+OWS.m */; }; 348F3A4F1E4A533900750D44 /* CallInterstitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */; }; 34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; }; + 4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */; }; + 4505C2C01E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */; }; + 4505C2C21E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2C11E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift */; }; + 4505C2C31E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2C11E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift */; }; + 4505C2C51E64977D00CEBF41 /* ExperienceUpgradeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2C41E64977D00CEBF41 /* ExperienceUpgradeViewController.swift */; }; + 4505C2C61E64977D00CEBF41 /* ExperienceUpgradeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2C41E64977D00CEBF41 /* ExperienceUpgradeViewController.swift */; }; 450873C31D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */; }; 450873C41D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */; }; 450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */; }; @@ -82,6 +88,8 @@ 45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */; }; 45AE48521E0732D6004D96C2 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */; }; 45B201761DAECBFE00C461E0 /* HighlightableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B201751DAECBFE00C461E0 /* HighlightableLabel.swift */; }; + 45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */; }; + 45BB93391E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */; }; 45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45BD60811DE9547E00A8F436 /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 45BFFFA81D898AF0004A12A7 /* OWSStaleNotificationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 45BFFFA71D898AF0004A12A7 /* OWSStaleNotificationObserver.m */; }; 45BFFFA91D898AF0004A12A7 /* OWSStaleNotificationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 45BFFFA71D898AF0004A12A7 /* OWSStaleNotificationObserver.m */; }; @@ -612,6 +620,9 @@ 348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInterstitialViewController.swift; sourceTree = ""; }; 34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = views/OWSAnyTouchGestureRecognizer.h; sourceTree = ""; }; 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSAnyTouchGestureRecognizer.m; path = views/OWSAnyTouchGestureRecognizer.m; sourceTree = ""; }; + 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExperienceUpgrade.swift; path = ExperienceUpgrades/ExperienceUpgrade.swift; sourceTree = ""; }; + 4505C2C11E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeFinder.swift; sourceTree = ""; }; + 4505C2C41E64977D00CEBF41 /* ExperienceUpgradeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeViewController.swift; sourceTree = ""; }; 450873C11D9D5149006B54F2 /* OWSExpirationTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirationTimerView.h; sourceTree = ""; }; 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSExpirationTimerView.m; sourceTree = ""; }; 450873C51D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIncomingMessageCollectionViewCell.h; sourceTree = ""; }; @@ -691,6 +702,7 @@ 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = ""; }; 45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Signal-Bridging-Header.h"; sourceTree = ""; }; 45B201751DAECBFE00C461E0 /* HighlightableLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightableLabel.swift; sourceTree = ""; }; + 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+featureSupport.swift"; sourceTree = ""; }; 45BD60811DE9547E00A8F436 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; }; 45BFFFA61D898AF0004A12A7 /* OWSStaleNotificationObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSStaleNotificationObserver.h; path = Observers/OWSStaleNotificationObserver.h; sourceTree = ""; }; 45BFFFA71D898AF0004A12A7 /* OWSStaleNotificationObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSStaleNotificationObserver.m; path = Observers/OWSStaleNotificationObserver.m; sourceTree = ""; }; @@ -719,7 +731,7 @@ 45EB32CD1D7465C900735B2E /* OWSLinkedDevicesTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSLinkedDevicesTableViewController.h; sourceTree = ""; }; 45EB32CE1D7465C900735B2E /* OWSLinkedDevicesTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSLinkedDevicesTableViewController.m; sourceTree = ""; }; 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSession.swift; sourceTree = ""; }; - 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallAudioSessionTest.swift; path = test/call/CallAudioSessionTest.swift; sourceTree = ""; }; + 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSessionTest.swift; sourceTree = ""; }; 45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RTCAudioSession.h; sourceTree = ""; }; 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioService.swift; sourceTree = ""; }; 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakTimer.swift; sourceTree = ""; }; @@ -1311,6 +1323,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4505C2BD1E648E6E00CEBF41 /* ExperienceUpgrades */ = { + isa = PBXGroup; + children = ( + 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */, + 4505C2C11E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift */, + ); + name = ExperienceUpgrades; + sourceTree = ""; + }; 450DF2061E0DD28D003D14BE /* UserInterface */ = { isa = PBXGroup; children = ( @@ -1589,6 +1610,7 @@ 76EB041118170B33006006FC /* environment */ = { isa = PBXGroup; children = ( + 4505C2BD1E648E6E00CEBF41 /* ExperienceUpgrades */, 45666F731D9BFDB9008FE134 /* Migrations */, B6258B311C29E2E60014138E /* NotificationsManager.h */, B6258B321C29E2E60014138E /* NotificationsManager.m */, @@ -2033,6 +2055,7 @@ FC3196311A08141D0094C78E /* Settings */, FC3196321A08142D0094C78E /* Signals */, FCFD25791A1543D500F4C644 /* Signup */, + 4505C2C41E64977D00CEBF41 /* ExperienceUpgradeViewController.swift */, ); name = "View Controllers"; path = "view controllers"; @@ -2212,6 +2235,7 @@ B660F6731C29867F00687D6E /* call */ = { isa = PBXGroup; children = ( + 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */, 456F6E2E1E261D1000FD2210 /* PeerConnectionClientTest.swift */, B660F6741C29867F00687D6E /* RecentCallTest.m */, ); @@ -2492,7 +2516,6 @@ B660F66C1C29867F00687D6E /* test */, D221A094169C9E5E00537ABF /* Supporting Files */, B66DBF4919D5BBC8006EA940 /* Images.xcassets */, - 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */, ); path = Signal; sourceTree = ""; @@ -2656,6 +2679,7 @@ EF764C341DB67CC5000D9A87 /* UIViewController+CameraPermissions.m */, 344F2F651E57A932000D9322 /* UIViewController+OWS.h */, 344F2F661E57A932000D9322 /* UIViewController+OWS.m */, + 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */, ); name = "UI Categories"; path = ..; @@ -3071,6 +3095,7 @@ 76EB061218170B33006006FC /* LoggingUtil.m in Sources */, 76EB060E18170B33006006FC /* DecayingSampleEstimator.m in Sources */, 340757C21E5602D6001F15DD /* AttachmentSharing.m in Sources */, + 4505C2C21E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift in Sources */, 76EB05BA18170B33006006FC /* CommitPacket.m in Sources */, 344F2F671E57A932000D9322 /* UIViewController+OWS.m in Sources */, 76EB060218170B33006006FC /* InitiatorSessionDescriptor.m in Sources */, @@ -3093,6 +3118,7 @@ 4531C9C41DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m in Sources */, 4516E3FF1DD2193B00DC4206 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */, 76EB05A818170B33006006FC /* RtpSocket.m in Sources */, + 4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */, 45387B041E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */, E197B61818BBEC1A00F073E5 /* RemoteIOAudio.m in Sources */, B67ADDC41989FF8700E1A773 /* RPServerRequestsManager.m in Sources */, @@ -3184,6 +3210,7 @@ E197B61118BBEC1A00F073E5 /* AudioProcessor.m in Sources */, FCAC964019FEF99A0046DFC5 /* InboxTableViewCell.m in Sources */, 76EB05EA18170B33006006FC /* CallProgress.m in Sources */, + 4505C2C51E64977D00CEBF41 /* ExperienceUpgradeViewController.swift in Sources */, 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */, FCFA64B41A24F3880007FB87 /* UIColor+OWS.m in Sources */, 4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */, @@ -3242,6 +3269,7 @@ 45EB32CF1D7465C900735B2E /* OWSLinkedDevicesTableViewController.m in Sources */, 45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */, B63761EE19E1FBE8005735D1 /* HttpRequestUtil.m in Sources */, + 45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */, 458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */, 451DE9FD1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */, 76EB05B618170B33006006FC /* MasterSecret.m in Sources */, @@ -3305,6 +3333,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4505C2C31E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift in Sources */, B660F7001C29988E00687D6E /* AppAudioManager.m in Sources */, B660F7011C29988E00687D6E /* AudioRouter.m in Sources */, B660F7021C29988E00687D6E /* AudioPacker.m in Sources */, @@ -3346,6 +3375,7 @@ B660F71D1C29988E00687D6E /* LocalizableText.m in Sources */, B660F71F1C29988E00687D6E /* PropertyListPreferences.m in Sources */, B660F7201C29988E00687D6E /* Release.m in Sources */, + 45BB93391E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */, B660F7211C29988E00687D6E /* SignalKeyingStorage.m in Sources */, B660F7221C29988E00687D6E /* VersionMigrations.m in Sources */, B660F7231C29988E00687D6E /* DnsManager.m in Sources */, @@ -3522,11 +3552,13 @@ B660F6CD1C29868000687D6E /* UdpSocketTest.m in Sources */, B660F6DD1C29868000687D6E /* ObservableTest.m in Sources */, B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */, + 4505C2C01E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */, B660F6C61C29868000687D6E /* MasterSecretTest.m in Sources */, B660F6D91C29868000687D6E /* CyclicalBufferTest.m in Sources */, B660F6DC1C29868000687D6E /* FutureUtilTest.m in Sources */, 450873C81D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */, B660F6CA1C29868000687D6E /* LowLatencyConnectorTest.m in Sources */, + 4505C2C61E64977D00CEBF41 /* ExperienceUpgradeViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Signal/Images.xcassets/introductory_splash_callkit.imageset/Contents.json b/Signal/Images.xcassets/introductory_splash_callkit.imageset/Contents.json new file mode 100644 index 000000000..52a3698e9 --- /dev/null +++ b/Signal/Images.xcassets/introductory_splash_callkit.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "signal-answer.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "signal-answer@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "signal-answer@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer.png b/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer.png new file mode 100644 index 000000000..d555b2988 Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer.png differ diff --git a/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer@2x.png b/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer@2x.png new file mode 100644 index 000000000..6be5c84fe Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer@2x.png differ diff --git a/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer@3x.png b/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer@3x.png new file mode 100644 index 000000000..30bda1070 Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_callkit.imageset/signal-answer@3x.png differ diff --git a/Signal/Images.xcassets/introductory_splash_video_calling.imageset/Contents.json b/Signal/Images.xcassets/introductory_splash_video_calling.imageset/Contents.json new file mode 100644 index 000000000..ac113690f --- /dev/null +++ b/Signal/Images.xcassets/introductory_splash_video_calling.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "signal-video-splash.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "signal-video-splash@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "signal-video-splash@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash.png b/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash.png new file mode 100644 index 000000000..4ed819309 Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash.png differ diff --git a/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash@2x.png b/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash@2x.png new file mode 100644 index 000000000..da4e74fe9 Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash@2x.png differ diff --git a/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash@3x.png b/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash@3x.png new file mode 100644 index 000000000..ef3580cea Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_video_calling.imageset/signal-video-splash@3x.png differ diff --git a/Signal/src/UIDevice+featureSupport.swift b/Signal/src/UIDevice+featureSupport.swift new file mode 100644 index 000000000..67c30fe90 --- /dev/null +++ b/Signal/src/UIDevice+featureSupport.swift @@ -0,0 +1,11 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation + +extension UIDevice { + var supportsCallKit: Bool { + return ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 10, minorVersion: 0, patchVersion: 0)) + } +} diff --git a/Signal/src/UIView+OWS.h b/Signal/src/UIView+OWS.h index f885d1c44..c97459ec3 100644 --- a/Signal/src/UIView+OWS.h +++ b/Signal/src/UIView+OWS.h @@ -49,4 +49,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); - (void)addBorderWithColor:(UIColor *)color; - (void)addRedBorder; +// Add red border to self, and all subviews recursively. +- (void)addRedBorderRecursively; + @end diff --git a/Signal/src/UIView+OWS.m b/Signal/src/UIView+OWS.m index 87cfe1458..2c34c25ee 100644 --- a/Signal/src/UIView+OWS.m +++ b/Signal/src/UIView+OWS.m @@ -166,4 +166,12 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) [self addBorderWithColor:[UIColor redColor]]; } +- (void)addRedBorderRecursively +{ + [self addRedBorder]; + for (UIView *subview in self.subviews) { + [subview addRedBorderRecursively]; + } +} + @end diff --git a/Signal/src/environment/ExperienceUpgradeFinder.swift b/Signal/src/environment/ExperienceUpgradeFinder.swift new file mode 100644 index 000000000..a3c230b88 --- /dev/null +++ b/Signal/src/environment/ExperienceUpgradeFinder.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation + +class ExperienceUpgradeFinder: NSObject { + public let TAG = "[ExperienceUpgradeFinder]" + + // Keep these ordered by increasing uniqueId. + private var allExperienceUpgrades: [ExperienceUpgrade] { + var upgrades = [ExperienceUpgrade(uniqueId: "001", + title: NSLocalizedString("UPGRADE_EXPERIENCE_VIDEO_TITLE", comment: "Header for upgrade experience"), + body: NSLocalizedString("UPGRADE_EXPERIENCE_VIDEO_DESCRIPTION", comment: "Description of video calling to upgrading (existing) users"), + image: #imageLiteral(resourceName: "introductory_splash_video_calling"))] + + if UIDevice.current.supportsCallKit { + upgrades.append(ExperienceUpgrade(uniqueId: "002", + title: NSLocalizedString("UPGRADE_EXPERIENCE_CALLKIT_TITLE", comment: "Header for upgrade experience"), + body: NSLocalizedString("UPGRADE_EXPERIENCE_CALLKIT_DESCRIPTION", comment: "Description of CallKit to upgrading (existing) users"), + image: #imageLiteral(resourceName: "introductory_splash_callkit"))) + } + + return upgrades + } + + + // MARK: - Instance Methods + + public func allUnseen(transaction: YapDatabaseReadTransaction) -> [ExperienceUpgrade] { + return allExperienceUpgrades.filter { ExperienceUpgrade.fetch(withUniqueID: $0.uniqueId, transaction: transaction) == nil } + } + + public func markAllAsSeen(transaction: YapDatabaseReadWriteTransaction) { + Logger.info("\(TAG) marking experience upgrades as seen") + allExperienceUpgrades.forEach { $0.save(with: transaction) } + } +} diff --git a/Signal/src/environment/ExperienceUpgrades/ExperienceUpgrade.swift b/Signal/src/environment/ExperienceUpgrades/ExperienceUpgrade.swift new file mode 100644 index 000000000..28117c6ba --- /dev/null +++ b/Signal/src/environment/ExperienceUpgrades/ExperienceUpgrade.swift @@ -0,0 +1,69 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation + +class ExperienceUpgrade: TSYapDatabaseObject { + let title: String + let body: String + let image: UIImage? + var seenAt: Date? + + required init(uniqueId: String, title: String, body: String, image: UIImage) { + self.title = title + self.body = body + self.image = image + super.init(uniqueId: uniqueId) + } + + override required init(uniqueId: String) { + // This is the unfortunate seam between strict swift and fast-and-loose objc + // we can't leave these properties nil, since we really "don't know" that the superclass + // will assign them. + self.title = "New Feature" + self.body = "Bug fixes and performance improvements." + self.image = nil + super.init(uniqueId: uniqueId) + } + + required init!(coder: NSCoder!) { + // This is the unfortunate seam between strict swift and fast-and-loose objc + // we can't leave these properties nil, since we really "don't know" that the superclass + // will assign them. + self.title = "New Feature" + self.body = "Bug fixes and performance improvements." + self.image = nil + super.init(coder: coder) + } + + required init(dictionary dictionaryValue: [AnyHashable : Any]!) throws { + // This is the unfortunate seam between strict swift and fast-and-loose objc + // we can't leave these properties nil, since we really "don't know" that the superclass + // will assign them. + self.title = "New Feature" + self.body = "Bug fixes and performance improvements." + self.image = nil + try super.init(dictionary: dictionaryValue) + } + + override class func storageBehaviorForProperty(withKey propertyKey: String) -> MTLPropertyStorage { + // These exist in a hardcoded set - no need to save them, plus it allows us to + // update copy/image down the line if there was a typo and we want to re-expose + // these models in a "change log" archive. + if propertyKey == "title" || propertyKey == "body" || propertyKey == "image" { + return MTLPropertyStorageNone + } else if propertyKey == "uniqueId" || propertyKey == "seenAt" { + return super.storageBehaviorForProperty(withKey: propertyKey) + } else { + // Being conservative here in case we rename a property. + assertionFailure("unknown property \(propertyKey)") + return super.storageBehaviorForProperty(withKey: propertyKey) + } + } + + func markAsSeen(transaction: YapDatabaseReadWriteTransaction) { + self.seenAt = Date() + super.save(with: transaction) + } +} diff --git a/Signal/src/view controllers/CallViewController.swift b/Signal/src/view controllers/CallViewController.swift index 2aea206f8..736725f2c 100644 --- a/Signal/src/view controllers/CallViewController.swift +++ b/Signal/src/view controllers/CallViewController.swift @@ -910,7 +910,7 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R return } else if !ignoreNag && call.direction == .incoming && - supportsCallKit() && + UIDevice.current.supportsCallKit && (!Environment.getCurrent().preferences.isCallKitEnabled() || Environment.getCurrent().preferences.isCallKitPrivacyEnabled()) { @@ -948,13 +948,7 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R self.dismiss(animated: false, completion:completion) } } - - // MARK: - Util - - private func supportsCallKit() -> Bool { - return ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 10, minorVersion: 0, patchVersion: 0)) - } - + // MARK: - CallServiceObserver internal func didUpdateCall(call: SignalCall?) { diff --git a/Signal/src/view controllers/ExperienceUpgradeViewController.swift b/Signal/src/view controllers/ExperienceUpgradeViewController.swift new file mode 100644 index 000000000..723d9d3ee --- /dev/null +++ b/Signal/src/view controllers/ExperienceUpgradeViewController.swift @@ -0,0 +1,292 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation + +class ExperienceUpgradeViewController: UIViewController, UIScrollViewDelegate { + let TAG = "[ExperienceUpgradeViewController]" + + private let experienceUpgrades: [ExperienceUpgrade] + + private var nextButton: UIButton! + private var previousButton: UIButton! + + // MARK: - Initializers + required init(experienceUpgrades: [ExperienceUpgrade]) { + self.experienceUpgrades = experienceUpgrades + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable, message:"unavailable, use initWithExperienceUpgrade instead") + required init?(coder aDecoder: NSCoder) { + assert(false) + // This should never happen, but so as not to explode we give some bogus data + self.experienceUpgrades = [ExperienceUpgrade()] + super.init(coder: aDecoder) + } + + // MARK: - View lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + addDismissGesture() + showCurrentSlide() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Avoid any possible vertical scrolling, which feels weird for carousel which should only swipe left/right + // if our carousel content is properly sized to be <= the carousel height. When written this wasn't be an issue, + // but it would be easy to introduce a small layout issue in the future which oversizes the content. + self.carouselView.contentSize = CGSize(width: self.carouselView.contentSize.width, // use actual content width + height: self.carouselView.frame.size.height) // but crop height to frame + } + + override func loadView() { + self.view = UIView() + view.backgroundColor = UIColor.white + + let splashView = UIView() + view.addSubview(splashView) + splashView.autoPinEdgesToSuperviewEdges() + + let carouselView = UIScrollView() + carouselView.delegate = self + self.carouselView = carouselView + splashView.addSubview(carouselView) + self.carouselView.isPagingEnabled = true + carouselView.showsHorizontalScrollIndicator = false + carouselView.showsVerticalScrollIndicator = false + carouselView.bounces = false + + // CarouselView layout + carouselView.autoPinEdge(toSuperviewEdge: .top) + carouselView.autoPinEdge(toSuperviewEdge: .left) + carouselView.autoPinEdge(toSuperviewEdge: .right) + + // Build slides for carousel + var previousSlideView: UIView? + for experienceUpgrade in experienceUpgrades { + let slideView = buildSlideView(header: experienceUpgrade.title, body: experienceUpgrade.body, image: experienceUpgrade.image) + carouselView.addSubview(slideView) + + slideView.autoPinEdge(toSuperviewEdge: .top, withInset: ScaleFromIPhone5(10)) + slideView.autoPinEdge(toSuperviewEdge: .bottom) + slideView.autoMatch(.width, to: .width, of: carouselView) + + // pin first slide to the superview + if previousSlideView == nil { + slideView.autoPinEdge(toSuperviewEdge: .left) + } else { + // pin any subsequent slide to the preveding slide + slideView.autoPinEdge(.left, to: .right, of: previousSlideView!) + } + + previousSlideView = slideView + } + // we should never be presenting a blank slideshow. + // but if so, we don't want to crash in prod. + assert(previousSlideView != nil) + + // ping the last slide to the superview right. + previousSlideView?.autoPinEdge(toSuperviewEdge: .right) + + // Previous button + + // Lightening the arrows' color to balance their heavy stroke with the thinner text on the page. + let arrowButtonColor = UIColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1) + let previousButton = UIButton() + self.previousButton = previousButton + splashView.addSubview(previousButton) + previousButton.isUserInteractionEnabled = true + previousButton.setTitleColor(arrowButtonColor, for: .normal) + previousButton.accessibilityLabel = NSLocalizedString("UPGRADE_CAROUSEL_PREVIOUS_BUTTON", comment: "accessibility label for arrow in slideshow") + previousButton.setTitle("‹", for: .normal) + previousButton.titleLabel?.font = UIFont.ows_mediumFont(withSize: ScaleFromIPhone5To7Plus(24, 48)) + previousButton.addTarget(self, action:#selector(didTapPreviousButton), for: .touchUpInside) + + // Previous button layout + previousButton.autoPinEdge(toSuperviewEdge: .left) + let arrowButtonInset = ScaleFromIPhone5(200) + previousButton.autoPinEdge(toSuperviewEdge: .top, withInset: arrowButtonInset) + + // Next button + let nextButton = UIButton() + self.nextButton = nextButton + splashView.addSubview(nextButton) + nextButton.isUserInteractionEnabled = true + nextButton.setTitleColor(arrowButtonColor, for: .normal) + nextButton.accessibilityLabel = NSLocalizedString("UPGRADE_CAROUSEL_NEXT_BUTTON", comment: "accessibility label for arrow in slideshow") + nextButton.setTitle("›", for: .normal) + nextButton.titleLabel?.font = UIFont.ows_mediumFont(withSize: ScaleFromIPhone5To7Plus(24, 48)) + nextButton.addTarget(self, action:#selector(didTapNextButton), for: .touchUpInside) + + // Next button layout + nextButton.autoPinEdge(toSuperviewEdge: .right) + nextButton.autoPinEdge(toSuperviewEdge: .top, withInset: arrowButtonInset) + + // Dismiss button + let dismissButton = UIButton() + splashView.addSubview(dismissButton) + dismissButton.setTitle(NSLocalizedString("DISMISS_BUTTON_TEXT", comment: ""), for: .normal) + dismissButton.setTitleColor(UIColor.ows_materialBlue(), for: .normal) + dismissButton.isUserInteractionEnabled = true + dismissButton.addTarget(self, action:#selector(didTapDismissButton), for: .touchUpInside) + + // Dismiss button layout + dismissButton.autoPinWidthToSuperview() + dismissButton.autoPinEdge(.top, to: .bottom, of: carouselView, withOffset: ScaleFromIPhone5(16)) + dismissButton.autoPinEdge(toSuperviewEdge: .bottom, withInset: ScaleFromIPhone5(32)) + } + + private func buildSlideView(header: String, body: String, image: UIImage?) -> UIView { + Logger.debug("\(TAG) in \(#function)") + + let containerView = UIView() + let headerView = UIView() + containerView.addSubview(headerView) + headerView.backgroundColor = UIColor.ows_materialBlue() + + headerView.autoPinWidthToSuperview() + headerView.autoPinEdge(toSuperviewEdge: .top, withInset: -16) + headerView.autoSetDimension(.height, toSize: ScaleFromIPhone5(100)) + + // Title label + let titleLabel = UILabel() + headerView.addSubview(titleLabel) + titleLabel.text = header + titleLabel.textAlignment = .center + titleLabel.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(26, 32)) + titleLabel.textColor = UIColor.white + titleLabel.minimumScaleFactor = 0.5 + titleLabel.adjustsFontSizeToFitWidth = true; + + // Title label layout + titleLabel.autoPinWidthToSuperview(withMargin: ScaleFromIPhone5To7Plus(16, 24)) + titleLabel.autoPinEdge(toSuperviewEdge: .bottom, withInset: ScaleFromIPhone5To7Plus(24, 32)) + + let slideView = UIView() + containerView.addSubview(slideView) + + let containerPadding = ScaleFromIPhone5To7Plus(12, 24) + slideView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: containerPadding, left: containerPadding, bottom: containerPadding, right: containerPadding)) + + // Body label + let bodyLabel = UILabel() + slideView.addSubview(bodyLabel) + bodyLabel.text = body + bodyLabel.font = UIFont.ows_lightFont(withSize: ScaleFromIPhone5To7Plus(18, 22)) + bodyLabel.textColor = UIColor.black + bodyLabel.numberOfLines = 0 + bodyLabel.textAlignment = .center + + // Body label layout + bodyLabel.autoPinWidthToSuperview() + bodyLabel.sizeToFit() + + // Image + let imageView = UIImageView(image: image) + slideView.addSubview(imageView) + imageView.contentMode = .scaleAspectFit + + // Image layout + imageView.autoPinWidthToSuperview() + imageView.autoSetDimension(.height, toSize: ScaleFromIPhone5To7Plus(200, 280)) + imageView.autoPinEdge(.top, to: .bottom, of: headerView, withOffset: ScaleFromIPhone5To7Plus(24, 32)) + imageView.autoPinEdge(.bottom, to: .top, of: bodyLabel, withOffset: -ScaleFromIPhone5To7Plus(24, 32)) + + return containerView + } + + private func addDismissGesture() { + let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleDismissGesture)) + swipeGesture.direction = .down + view.addGestureRecognizer(swipeGesture) + } + + // MARK: Carousel + + private var carouselView: UIScrollView! + private var currentSlideIndex = 0 + + private func showNextSlide() { + guard hasNextSlide() else { + Logger.debug("\(TAG) no next slide to show") + return; + } + + currentSlideIndex += 1 + showCurrentSlide() + } + + private func showPreviousSlide() { + guard hasPreviousSlide() else { + Logger.debug("\(TAG) no previous slide to show") + return + } + + currentSlideIndex -= 1 + showCurrentSlide() + } + + private func hasPreviousSlide() -> Bool { + return currentSlideIndex > 0 + } + + private func hasNextSlide() -> Bool { + return currentSlideIndex < experienceUpgrades.count - 1 + } + + private func updateSlideControls() { + self.nextButton.isHidden = !hasNextSlide() + self.previousButton.isHidden = !hasPreviousSlide() + } + + private func showCurrentSlide() { + Logger.debug("\(TAG) showing slide: \(currentSlideIndex)") + updateSlideControls() + + // update the scroll view to the appropriate page + let bounds = carouselView.bounds + + let point = CGPoint(x: bounds.width * CGFloat(currentSlideIndex), y: 0) + let pageBounds = CGRect(origin: point, size: bounds.size) + + carouselView.scrollRectToVisible(pageBounds, animated: true) + } + + // MARK: - Actions + + func didTapNextButton(sender: UIButton) { + Logger.debug("\(TAG) in \(#function)") + showNextSlide() + } + + func didTapPreviousButton(sender: UIButton) { + Logger.debug("\(TAG) in \(#function)") + showPreviousSlide() + } + + func didTapDismissButton(sender: UIButton) { + Logger.debug("\(TAG) in \(#function)") + self.dismiss(animated: true) + } + + func handleDismissGesture(sender: AnyObject) { + Logger.debug("\(TAG) in \(#function)") + self.dismiss(animated: true) + } + + // MARK: - ScrollViewDelegate + + // Update the slider controls to reflect which page we think we'll end up on. + // we use WillEndDragging instead of (e.g.) didEndDecelerating, else the switch feels too late. + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + let pageWidth = scrollView.frame.size.width + let page = floor(targetContentOffset.pointee.x / pageWidth) + currentSlideIndex = Int(page) + updateSlideControls() + } +} diff --git a/Signal/src/view controllers/PrivacySettingsTableViewController.m b/Signal/src/view controllers/PrivacySettingsTableViewController.m index 28d50fb24..4d1561bb1 100644 --- a/Signal/src/view controllers/PrivacySettingsTableViewController.m +++ b/Signal/src/view controllers/PrivacySettingsTableViewController.m @@ -136,9 +136,9 @@ typedef NS_ENUM(NSInteger, PrivacySettingsTableViewControllerSectionIndex) { case PrivacySettingsTableViewControllerSectionIndexCalling: return 1; case PrivacySettingsTableViewControllerSectionIndexCallKitEnabled: - return self.supportsCallKit ? 1 : 0; + return [UIDevice currentDevice].supportsCallKit ? 1 : 0; case PrivacySettingsTableViewControllerSectionIndexCallKitPrivacy: - return (self.supportsCallKit && [[Environment getCurrent].preferences isCallKitEnabled]) ? 1 : 0; + return ([UIDevice currentDevice].supportsCallKit && [[Environment getCurrent].preferences isCallKitEnabled]) ? 1 : 0; case PrivacySettingsTableViewControllerSectionIndexHistoryLog: return 1; case PrivacySettingsTableViewControllerSectionIndexBlockOnIdentityChange: @@ -157,11 +157,11 @@ typedef NS_ENUM(NSInteger, PrivacySettingsTableViewControllerSectionIndex) { return NSLocalizedString(@"SETTINGS_CALLING_HIDES_IP_ADDRESS_PREFERENCE_TITLE_DETAIL", @"User settings section footer, a detailed explanation"); case PrivacySettingsTableViewControllerSectionIndexCallKitEnabled: - return (self.supportsCallKit + return ([UIDevice currentDevice].supportsCallKit ? NSLocalizedString(@"SETTINGS_SECTION_CALL_KIT_DESCRIPTION", @"Settings table section footer.") : nil); case PrivacySettingsTableViewControllerSectionIndexCallKitPrivacy: - return ((self.supportsCallKit && [[Environment getCurrent].preferences isCallKitEnabled]) + return (([UIDevice currentDevice].supportsCallKit && [[Environment getCurrent].preferences isCallKitEnabled]) ? NSLocalizedString(@"SETTINGS_SECTION_CALL_KIT_PRIVACY_DESCRIPTION", @"Explanation of the 'CallKit Privacy` preference.") : nil); @@ -274,13 +274,6 @@ typedef NS_ENUM(NSInteger, PrivacySettingsTableViewControllerSectionIndex) { [[Environment getCurrent].preferences setIsCallKitPrivacyEnabled:!sender.isOn]; } -#pragma mark - Util - -- (BOOL)supportsCallKit -{ - return SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(10, 0); -} - #pragma mark - Log util + (NSString *)tag diff --git a/Signal/src/view controllers/SignalsViewController.m b/Signal/src/view controllers/SignalsViewController.m index 2cfc776c1..6714306ed 100644 --- a/Signal/src/view controllers/SignalsViewController.m +++ b/Signal/src/view controllers/SignalsViewController.m @@ -39,8 +39,12 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS @property (nonatomic) long inboxCount; @property (nonatomic, retain) UISegmentedControl *segmentedControl; @property (nonatomic, strong) id previewingContext; + +// Dependencies + @property (nonatomic, readonly, strong) AccountManager *accountManager; @property (nonatomic, readonly) OWSContactsManager *contactsManager; +@property (nonatomic, readonly) ExperienceUpgradeFinder *experienceUpgradeFinder; @property (nonatomic, readonly) TSMessagesManager *messagesManager; @property (nonatomic, readonly, strong) OWSMessageSender *messageSender; @@ -78,6 +82,7 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS _contactsManager = [Environment getCurrent].contactsManager; _messagesManager = [TSMessagesManager sharedManager]; _messageSender = [Environment getCurrent].messageSender; + _experienceUpgradeFinder = [ExperienceUpgradeFinder new]; } - (void)awakeFromNib @@ -304,10 +309,18 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (self.newlyRegisteredUser) { + [self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) { + [self.experienceUpgradeFinder markAllAsSeenWithTransaction:transaction]; + }]; + [self didAppearForNewlyRegisteredUser]; + } else { + [self displayAnyUnseenUpgradeExperience]; } } +#pragma mark - startup + - (void)didAppearForNewlyRegisteredUser { ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus(); @@ -341,6 +354,25 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS } } +- (void)displayAnyUnseenUpgradeExperience +{ + AssertIsOnMainThread(); + + __block NSArray *unseenUpgrades; + [self.editingDbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + unseenUpgrades = [self.experienceUpgradeFinder allUnseenWithTransaction:transaction]; + }]; + + if (unseenUpgrades.count > 0) { + ExperienceUpgradeViewController *experienceUpgradeViewController = [[ExperienceUpgradeViewController alloc] initWithExperienceUpgrades:unseenUpgrades]; + [self presentViewController:experienceUpgradeViewController animated:YES completion:^{ + [self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) { + [self.experienceUpgradeFinder markAllAsSeenWithTransaction:transaction]; + }]; + }]; + } +} + - (void)ensureNotificationsUpToDate { OWSSyncPushTokensJob *syncPushTokensJob = diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 009eb0b16..3629a6c74 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -791,10 +791,10 @@ "SETTINGS_BUTTON_ACCESSIBILITY" = "Settings"; /* Table cell label */ -"SETTINGS_CALLING_HIDES_IP_ADDRESS_PREFERENCE_TITLE" = "Hide IP Address"; +"SETTINGS_CALLING_HIDES_IP_ADDRESS_PREFERENCE_TITLE" = "Always Relay Calls"; /* User settings section footer, a detailed explanation */ -"SETTINGS_CALLING_HIDES_IP_ADDRESS_PREFERENCE_TITLE_DETAIL" = "Hiding your IP address during audio and video calls will decrease call quality."; +"SETTINGS_CALLING_HIDES_IP_ADDRESS_PREFERENCE_TITLE_DETAIL" = "Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality."; /* No comment provided by engineer. */ "SETTINGS_CLEAR_HISTORY" = "Clear History Logs"; @@ -836,7 +836,7 @@ "SETTINGS_PRIVACY_CALLKIT_PRIVACY_TITLE" = "Show Names & Numbers"; /* Short table cell label */ -"SETTINGS_PRIVACY_CALLKIT_TITLE" = "Use CallKit"; +"SETTINGS_PRIVACY_CALLKIT_TITLE" = "iOS Call Integration"; /* No comment provided by engineer. */ "SETTINGS_PRIVACY_TITLE" = "Privacy"; @@ -848,10 +848,10 @@ "SETTINGS_SCREEN_SECURITY" = "Enable Screen Security"; /* No comment provided by engineer. */ -"SETTINGS_SCREEN_SECURITY_DETAIL" = "Prevents Signal previews from appearing in the app switcher."; +"SETTINGS_SCREEN_SECURITY_DETAIL" = "Prevent Signal previews from appearing in the app switcher."; /* Settings table section footer. */ -"SETTINGS_SECTION_CALL_KIT_DESCRIPTION" = "CallKit allows you to answer calls directly from your lockscreen. Be aware that when using CallKit, Apple syncs some call metadata to your iCloud account."; +"SETTINGS_SECTION_CALL_KIT_DESCRIPTION" = "Show Signal calls on your lock screen and in the system's call history. If iCloud is enabled, call history will be shared with Apple."; /* Explanation of the 'CallKit Privacy` preference. */ "SETTINGS_SECTION_CALL_KIT_PRIVACY_DESCRIPTION" = "If you show the names and phone numbers of incoming callers in CallKit, this information will appear in your phone's call history and will be synced to your iCloud account."; @@ -949,6 +949,24 @@ /* No comment provided by engineer. */ "UPDATE_BUTTON_TITLE" = "Update"; +/* accessibility label for arrow in slideshow */ +"UPGRADE_CAROUSEL_NEXT_BUTTON" = "Show next"; + +/* accessibility label for arrow in slideshow */ +"UPGRADE_CAROUSEL_PREVIOUS_BUTTON" = "Show previous"; + +/* Description of CallKit to upgrading (existing) users */ +"UPGRADE_EXPERIENCE_CALLKIT_DESCRIPTION" = "Answering calls from your lockscreen is easy and private, since by default we anonymize your caller.\n\n Read more in your privacy settings."; + +/* Header for upgrade experience */ +"UPGRADE_EXPERIENCE_CALLKIT_TITLE" = "Just Swipe to Answer"; + +/* Description of video calling to upgrading (existing) users */ +"UPGRADE_EXPERIENCE_VIDEO_DESCRIPTION" = "Signal now supports secure video calling. Just start a call like normal, tap the camera button, and wave hello."; + +/* Header for upgrade experience */ +"UPGRADE_EXPERIENCE_VIDEO_TITLE" = "Hello Secure Video Calls!"; + /* No comment provided by engineer. */ "Upgrading Signal ..." = "Upgrading Signal ...";