diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 0dba75b4d..2b4c0a184 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -506,6 +506,7 @@ 4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */; }; 4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC613352227A00400E21A3A /* ConversationSearch.swift */; }; 4CEB78C92178EBAB00F315D2 /* OWSSessionResetJobRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */; }; + 4CFD151D22415AA400F2450F /* CallVideoHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFD151C22415AA400F2450F /* CallVideoHintView.swift */; }; 4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */; }; 70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; }; 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; @@ -1256,6 +1257,7 @@ 4CEB78C72178EBAB00F315D2 /* OWSSessionResetJobRecord.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OWSSessionResetJobRecord.h; sourceTree = ""; }; 4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OWSSessionResetJobRecord.m; sourceTree = ""; }; 4CFB4E9B220BC56D00ECB4DE /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = translations/nb.lproj/Localizable.strings; sourceTree = ""; }; + 4CFD151C22415AA400F2450F /* CallVideoHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVideoHintView.swift; sourceTree = ""; }; 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LegacyNotificationsAdaptee.swift; path = UserInterface/Notifications/LegacyNotificationsAdaptee.swift; sourceTree = ""; }; 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActionsViewController.swift; sourceTree = ""; }; 69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = ""; }; @@ -1883,11 +1885,11 @@ 34B3F8331E8DF1700035BE1A /* ViewControllers */ = { isa = PBXGroup; children = ( + 4CFD151B22415A6C00F2450F /* Call */, 452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */, 340FC87A204DAC8C007AEB0F /* AppSettings */, 34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */, 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */, - 34B3F83B1E8DF1700035BE1A /* CallViewController.swift */, 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */, 348BB25C20A0C5530047AEC2 /* ContactShareViewHelper.swift */, 34B3F83E1E8DF1700035BE1A /* ContactsPicker.swift */, @@ -2335,6 +2337,15 @@ path = SSKTests; sourceTree = ""; }; + 4CFD151B22415A6C00F2450F /* Call */ = { + isa = PBXGroup; + children = ( + 34B3F83B1E8DF1700035BE1A /* CallViewController.swift */, + 4CFD151C22415AA400F2450F /* CallVideoHintView.swift */, + ); + path = Call; + sourceTree = ""; + }; 76EB03C118170B33006006FC /* src */ = { isa = PBXGroup; children = ( @@ -3551,6 +3562,7 @@ 348570A820F67575004FF32B /* OWSMessageHeaderView.m in Sources */, 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */, 34B6A907218B5241007C4606 /* TypingIndicatorCell.swift in Sources */, + 4CFD151D22415AA400F2450F /* CallVideoHintView.swift in Sources */, 34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */, 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */, 34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */, diff --git a/Signal/src/UserInterface/HapticFeedback.swift b/Signal/src/UserInterface/HapticFeedback.swift index beb47fd6a..2db0b0975 100644 --- a/Signal/src/UserInterface/HapticFeedback.swift +++ b/Signal/src/UserInterface/HapticFeedback.swift @@ -1,24 +1,37 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation -protocol HapticAdapter { +protocol SelectionHapticFeedbackAdapter { func selectionChanged() } -class LegacyHapticAdapter: NSObject, HapticAdapter { +class SelectionHapticFeedback: SelectionHapticFeedbackAdapter { + let adapter: SelectionHapticFeedbackAdapter - // MARK: HapticAdapter + init() { + if #available(iOS 10, *) { + adapter = ModernSelectionHapticFeedbackAdapter() + } else { + adapter = LegacySelectionHapticFeedbackAdapter() + } + } + func selectionChanged() { + adapter.selectionChanged() + } +} + +class LegacySelectionHapticFeedbackAdapter: NSObject, SelectionHapticFeedbackAdapter { func selectionChanged() { // do nothing } } @available(iOS 10, *) -class FeedbackGeneratorHapticAdapter: NSObject, HapticAdapter { +class ModernSelectionHapticFeedbackAdapter: NSObject, SelectionHapticFeedbackAdapter { let selectionFeedbackGenerator: UISelectionFeedbackGenerator override init() { @@ -34,18 +47,61 @@ class FeedbackGeneratorHapticAdapter: NSObject, HapticAdapter { } } -class HapticFeedback: HapticAdapter { - let adapter: HapticAdapter +enum NotificationHapticFeedbackType { + case error, success, warning +} + +extension NotificationHapticFeedbackType { + var uiNotificationFeedbackType: UINotificationFeedbackType { + switch self { + case .error: return .error + case .success: return .success + case .warning: return .warning + } + } +} + +protocol NotificationHapticFeedbackAdapter { + func notificationOccurred(_ notificationType: NotificationHapticFeedbackType) +} + +class NotificationHapticFeedback: NotificationHapticFeedbackAdapter { + + let adapter: NotificationHapticFeedbackAdapter init() { if #available(iOS 10, *) { - adapter = FeedbackGeneratorHapticAdapter() + adapter = ModernNotificationHapticFeedbackAdapter() } else { - adapter = LegacyHapticAdapter() + adapter = LegacyNotificationHapticFeedbackAdapter() } } - func selectionChanged() { - adapter.selectionChanged() + func notificationOccurred(_ notificationType: NotificationHapticFeedbackType) { + adapter.notificationOccurred(notificationType) + } +} + +@available(iOS 10.0, *) +class ModernNotificationHapticFeedbackAdapter: NotificationHapticFeedbackAdapter { + let feedbackGenerator = UINotificationFeedbackGenerator() + + init() { + feedbackGenerator.prepare() + } + + func notificationOccurred(_ notificationType: NotificationHapticFeedbackType) { + feedbackGenerator.notificationOccurred(notificationType.uiNotificationFeedbackType) + feedbackGenerator.prepare() + } +} + +class LegacyNotificationHapticFeedbackAdapter: NotificationHapticFeedbackAdapter { + func notificationOccurred(_ notificationType: NotificationHapticFeedbackType) { + vibrate() + } + + private func vibrate() { + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) } } diff --git a/Signal/src/ViewControllers/Call/CallVideoHintView.swift b/Signal/src/ViewControllers/Call/CallVideoHintView.swift new file mode 100644 index 000000000..4a6809e97 --- /dev/null +++ b/Signal/src/ViewControllers/Call/CallVideoHintView.swift @@ -0,0 +1,83 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +protocol CallVideoHintViewDelegate: AnyObject { + func didTapCallVideoHintView(_ videoHintView: CallVideoHintView) +} + +class CallVideoHintView: UIView { + let label = UILabel() + var tapGesture: UITapGestureRecognizer! + weak var delegate: CallVideoHintViewDelegate? + + let kTailHMargin: CGFloat = 12 + let kTailWidth: CGFloat = 16 + let kTailHeight: CGFloat = 8 + + init() { + super.init(frame: .zero) + + tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:))) + addGestureRecognizer(tapGesture) + + let layerView = OWSLayerView() + let shapeLayer = CAShapeLayer() + shapeLayer.fillColor = UIColor.ows_signalBlue.cgColor + layerView.layer.addSublayer(shapeLayer) + addSubview(layerView) + layerView.autoPinEdgesToSuperviewEdges() + + let container = UIView() + addSubview(container) + container.autoSetDimension(.width, toSize: ScaleFromIPhone5(250), relation: .lessThanOrEqual) + container.layoutMargins = UIEdgeInsets(top: 7, leading: 12, bottom: 7, trailing: 12) + container.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 0, leading: 0, bottom: kTailHeight, trailing: 0)) + + container.addSubview(label) + label.autoPinEdgesToSuperviewMargins() + label.setCompressionResistanceHigh() + label.setContentHuggingHigh() + label.font = UIFont.ows_dynamicTypeBody + label.textColor = .ows_white + label.numberOfLines = 0 + label.text = NSLocalizedString("CALL_VIEW_ENABLE_VIDEO_HINT", comment: "tooltip label when remote party has enabled their video") + + layerView.layoutCallback = { view in + let bezierPath = UIBezierPath() + + // Bubble + let bubbleBounds = container.bounds + bezierPath.append(UIBezierPath(roundedRect: bubbleBounds, cornerRadius: 8)) + + // Tail + var tailBottom = CGPoint(x: self.kTailHMargin + self.kTailWidth * 0.5, y: view.height()) + var tailLeft = CGPoint(x: self.kTailHMargin, y: view.height() - self.kTailHeight) + var tailRight = CGPoint(x: self.kTailHMargin + self.kTailWidth, y: view.height() - self.kTailHeight) + if (!CurrentAppContext().isRTL) { + tailBottom.x = view.width() - tailBottom.x + tailLeft.x = view.width() - tailLeft.x + tailRight.x = view.width() - tailRight.x + } + bezierPath.move(to: tailBottom) + bezierPath.addLine(to: tailLeft) + bezierPath.addLine(to: tailRight) + bezierPath.addLine(to: tailBottom) + shapeLayer.path = bezierPath.cgPath + shapeLayer.frame = view.bounds + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - + + @objc + func didTap(tapGesture: UITapGestureRecognizer) { + self.delegate?.didTapCallVideoHintView(self) + } +} diff --git a/Signal/src/ViewControllers/CallViewController.swift b/Signal/src/ViewControllers/Call/CallViewController.swift similarity index 96% rename from Signal/src/ViewControllers/CallViewController.swift rename to Signal/src/ViewControllers/Call/CallViewController.swift index cb6b5360b..f56a6936b 100644 --- a/Signal/src/ViewControllers/CallViewController.swift +++ b/Signal/src/ViewControllers/Call/CallViewController.swift @@ -88,10 +88,6 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, } } - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - // MARK: - Settings Nag Views var isShowingSettingsNag = false { @@ -230,12 +226,18 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, return .portrait } + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + // MARK: - Create Views func createViews() { self.view.isUserInteractionEnabled = true - self.view.addGestureRecognizer(OWSAnyTouchGestureRecognizer(target: self, - action: #selector(didTouchRootView))) + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, + action: #selector(didTouchRootView))) + + videoHintView.delegate = self // Dark blurred background. let blurEffect = UIBlurEffect(style: .dark) @@ -596,6 +598,8 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, updateCallUI(callState: call.state) } + let videoHintView = CallVideoHintView() + internal func updateLocalVideoLayout() { if !localVideoView.isHidden { localVideoView.superview?.bringSubview(toFront: localVideoView) @@ -744,10 +748,20 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, contactNameLabel.isHidden = true callStatusLabel.isHidden = true ongoingCallControls.isHidden = true + videoHintView.isHidden = true } else { leaveCallViewButton.isHidden = false contactNameLabel.isHidden = false callStatusLabel.isHidden = false + + if hasRemoteVideo && !hasLocalVideo && !hasShownLocalVideo && !hasUserDismissedVideoHint { + view.addSubview(videoHintView) + videoHintView.isHidden = false + videoHintView.autoPinEdge(.bottom, to: .top, of: audioModeVideoButton) + videoHintView.autoPinEdge(.trailing, to: .leading, of: audioModeVideoButton, withOffset: buttonSize() / 2 + videoHintView.kTailHMargin + videoHintView.kTailWidth / 2) + } else { + videoHintView.removeFromSuperview() + } } let doLocalVideoLayout = { @@ -1021,6 +1035,11 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, AssertIsOnMainThread() + guard localVideoView.captureSession != captureSession else { + Logger.debug("ignoring redundant update") + return + } + localVideoView.captureSession = captureSession let isHidden = captureSession == nil @@ -1040,9 +1059,13 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, return self.remoteVideoTrack != nil } + var hasUserDismissedVideoHint: Bool = false + internal func updateRemoteVideoTrack(remoteVideoTrack: RTCVideoTrack?) { AssertIsOnMainThread() + guard self.remoteVideoTrack != remoteVideoTrack else { + Logger.debug("ignoring redundant update") return } @@ -1051,11 +1074,32 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, remoteVideoView.renderFrame(nil) self.remoteVideoTrack = remoteVideoTrack self.remoteVideoTrack?.add(remoteVideoView) + shouldRemoteVideoControlsBeHidden = false + if remoteVideoTrack != nil { + playRemoteEnabledVideoHapticFeedback() + } + updateRemoteVideoLayout() } + // MARK: Video Haptics + + let feedbackGenerator = NotificationHapticFeedback() + var lastHapticTime: TimeInterval = CACurrentMediaTime() + func playRemoteEnabledVideoHapticFeedback() { + let currentTime = CACurrentMediaTime() + guard currentTime - lastHapticTime > 5 else { + Logger.debug("ignoring haptic feedback since it's too soon") + return + } + feedbackGenerator.notificationOccurred(.success) + lastHapticTime = currentTime + } + + // MARK: - Dismiss + internal func dismissIfPossible(shouldDelay: Bool, ignoreNag ignoreNagParam: Bool = false, completion: (() -> Void)? = nil) { callUIAdapter.audioService.delegate = nil @@ -1174,3 +1218,10 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, } } } + +extension CallViewController: CallVideoHintViewDelegate { + func didTapCallVideoHintView(_ videoHintView: CallVideoHintView) { + self.hasUserDismissedVideoHint = true + updateRemoteVideoLayout() + } +} diff --git a/Signal/src/ViewControllers/MenuActionsViewController.swift b/Signal/src/ViewControllers/MenuActionsViewController.swift index 4e04f6c76..d23799801 100644 --- a/Signal/src/ViewControllers/MenuActionsViewController.swift +++ b/Signal/src/ViewControllers/MenuActionsViewController.swift @@ -245,7 +245,7 @@ class MenuActionSheetView: UIView, MenuActionViewDelegate { private let actionStackView: UIStackView private var actions: [MenuAction] private var actionViews: [MenuActionView] - private var hapticFeedback: HapticFeedback + private var hapticFeedback: SelectionHapticFeedback private var hasEverHighlightedAction = false weak var delegate: MenuActionSheetDelegate? @@ -268,7 +268,7 @@ class MenuActionSheetView: UIView, MenuActionViewDelegate { actions = [] actionViews = [] - hapticFeedback = HapticFeedback() + hapticFeedback = SelectionHapticFeedback() super.init(frame: frame) diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 8b2e4cdeb..dfbc5329e 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -350,6 +350,9 @@ /* Accessibility label for declining incoming calls */ "CALL_VIEW_DECLINE_INCOMING_CALL_LABEL" = "Decline incoming call"; +/* tooltip label when remote party has enabled their video */ +"CALL_VIEW_ENABLE_VIDEO_HINT" = "Tap here to turn on your video"; + /* Accessibility label for hang up call */ "CALL_VIEW_HANGUP_LABEL" = "End call";