mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00

Added ability to download music without streaming Added progress indicators for various blocking tasks Fixed image gallery swipe to dismiss after zooming Added online member count indication in supergroups Fixed contact statuses in contact search
397 lines
15 KiB
Swift
397 lines
15 KiB
Swift
import Foundation
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
|
|
import LegacyComponents
|
|
|
|
private let offsetThreshold: CGFloat = 10.0
|
|
private let dismissOffsetThreshold: CGFloat = 70.0
|
|
|
|
enum ChatTextInputMediaRecordingButtonMode: Int32 {
|
|
case audio = 0
|
|
case video = 1
|
|
}
|
|
|
|
private func findTargetView(_ view: UIView, point: CGPoint) -> UIView? {
|
|
if view.bounds.contains(point) && view.tag == 0x01f2bca {
|
|
return view
|
|
}
|
|
for subview in view.subviews {
|
|
let frame = subview.frame
|
|
if let result = findTargetView(subview, point: point.offsetBy(dx: -frame.minX, dy: -frame.minY)) {
|
|
return result
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private final class ChatTextInputMediaRecordingButtonPresenterContainer: UIView {
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let result = findTargetView(self, point: point) {
|
|
return result
|
|
}
|
|
for subview in self.subviews {
|
|
if let result = subview.hitTest(point.offsetBy(dx: -subview.frame.minX, dy: -subview.frame.minY), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
}
|
|
|
|
private final class ChatTextInputMediaRecordingButtonPresenterController: ViewController {
|
|
private var controllerNode: ChatTextInputMediaRecordingButtonPresenterControllerNode {
|
|
return self.displayNode as! ChatTextInputMediaRecordingButtonPresenterControllerNode
|
|
}
|
|
|
|
var containerView: UIView? {
|
|
didSet {
|
|
if self.isNodeLoaded {
|
|
self.controllerNode.containerView = self.containerView
|
|
}
|
|
}
|
|
}
|
|
|
|
override func loadDisplayNode() {
|
|
self.displayNode = ChatTextInputMediaRecordingButtonPresenterControllerNode()
|
|
if let containerView = self.containerView {
|
|
self.controllerNode.containerView = containerView
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ChatTextInputMediaRecordingButtonPresenterControllerNode: ViewControllerTracingNode {
|
|
var containerView: UIView? {
|
|
didSet {
|
|
if self.containerView !== oldValue {
|
|
if self.isNodeLoaded, let containerView = oldValue, containerView.superview === self.view {
|
|
containerView.removeFromSuperview()
|
|
}
|
|
if self.isNodeLoaded, let containerView = self.containerView {
|
|
self.view.addSubview(containerView)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
if let containerView = self.containerView {
|
|
self.view.addSubview(containerView)
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let containerView = self.containerView {
|
|
if let result = containerView.hitTest(point, with: event), result !== containerView {
|
|
return result
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private final class ChatTextInputMediaRecordingButtonPresenter : NSObject, TGModernConversationInputMicButtonPresentation {
|
|
private let account: Account?
|
|
private let presentController: (ViewController) -> Void
|
|
private let container: ChatTextInputMediaRecordingButtonPresenterContainer
|
|
private var presentationController: ChatTextInputMediaRecordingButtonPresenterController?
|
|
|
|
init(account: Account, presentController: @escaping (ViewController) -> Void) {
|
|
self.account = account
|
|
self.presentController = presentController
|
|
self.container = ChatTextInputMediaRecordingButtonPresenterContainer()
|
|
}
|
|
|
|
deinit {
|
|
self.container.removeFromSuperview()
|
|
if let presentationController = self.presentationController {
|
|
presentationController.presentingViewController?.dismiss(animated: false, completion: {})
|
|
self.presentationController = nil
|
|
}
|
|
}
|
|
|
|
func view() -> UIView! {
|
|
return self.container
|
|
}
|
|
|
|
func setUserInteractionEnabled(_ enabled: Bool) {
|
|
self.container.isUserInteractionEnabled = enabled
|
|
}
|
|
|
|
func present() {
|
|
if let keyboardWindow = LegacyComponentsGlobals.provider().applicationKeyboardWindow(), !keyboardWindow.isHidden {
|
|
keyboardWindow.addSubview(self.container)
|
|
} else {
|
|
var presentNow = false
|
|
if self.presentationController == nil {
|
|
let presentationController = ChatTextInputMediaRecordingButtonPresenterController(navigationBarPresentationData: nil)
|
|
presentationController.statusBar.statusBarStyle = .Ignore
|
|
self.presentationController = presentationController
|
|
presentNow = true
|
|
}
|
|
|
|
self.presentationController?.containerView = self.container
|
|
if let presentationController = self.presentationController, presentNow {
|
|
self.presentController(presentationController)
|
|
}
|
|
}
|
|
}
|
|
|
|
func dismiss() {
|
|
self.container.removeFromSuperview()
|
|
if let presentationController = self.presentationController {
|
|
presentationController.presentingViewController?.dismiss(animated: false, completion: {})
|
|
self.presentationController = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButton, TGModernConversationInputMicButtonDelegate {
|
|
private var theme: PresentationTheme
|
|
|
|
var mode: ChatTextInputMediaRecordingButtonMode = .audio
|
|
var account: Account?
|
|
let presentController: (ViewController) -> Void
|
|
var recordingDisabled: () -> Void = { }
|
|
var beginRecording: () -> Void = { }
|
|
var endRecording: (Bool) -> Void = { _ in }
|
|
var stopRecording: () -> Void = { }
|
|
var offsetRecordingControls: () -> Void = { }
|
|
var switchMode: () -> Void = { }
|
|
var updateLocked: (Bool) -> Void = { _ in }
|
|
|
|
private var modeTimeoutTimer: SwiftSignalKit.Timer?
|
|
|
|
private let innerIconView: UIImageView
|
|
|
|
private var recordingOverlay: ChatTextInputAudioRecordingOverlay?
|
|
private var startTouchLocation: CGPoint?
|
|
private(set) var controlsOffset: CGFloat = 0.0
|
|
|
|
private var micLevelDisposable: MetaDisposable?
|
|
|
|
var audioRecorder: ManagedAudioRecorder? {
|
|
didSet {
|
|
if self.audioRecorder !== oldValue {
|
|
if self.micLevelDisposable == nil {
|
|
micLevelDisposable = MetaDisposable()
|
|
}
|
|
if let audioRecorder = self.audioRecorder {
|
|
self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in
|
|
Queue.mainQueue().async {
|
|
//self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
|
|
self?.addMicLevel(CGFloat(level))
|
|
}
|
|
}))
|
|
} else if self.videoRecordingStatus == nil {
|
|
self.micLevelDisposable?.set(nil)
|
|
}
|
|
|
|
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
|
|
}
|
|
}
|
|
}
|
|
|
|
var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
|
|
didSet {
|
|
if self.videoRecordingStatus !== oldValue {
|
|
if self.micLevelDisposable == nil {
|
|
micLevelDisposable = MetaDisposable()
|
|
}
|
|
|
|
if let videoRecordingStatus = self.videoRecordingStatus {
|
|
self.micLevelDisposable?.set(videoRecordingStatus.micLevel.start(next: { [weak self] level in
|
|
Queue.mainQueue().async {
|
|
//self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
|
|
self?.addMicLevel(CGFloat(level))
|
|
}
|
|
}))
|
|
} else if self.audioRecorder == nil {
|
|
self.micLevelDisposable?.set(nil)
|
|
}
|
|
|
|
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private var hasRecorder: Bool = false {
|
|
didSet {
|
|
if self.hasRecorder != oldValue {
|
|
if self.hasRecorder {
|
|
self.animateIn()
|
|
} else {
|
|
self.animateOut()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
init(theme: PresentationTheme, presentController: @escaping (ViewController) -> Void) {
|
|
self.theme = theme
|
|
self.innerIconView = UIImageView()
|
|
self.presentController = presentController
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
self.disablesInteractiveTransitionGestureRecognizer = true
|
|
|
|
let inputPanelTheme = theme.chat.inputPanel
|
|
|
|
self.pallete = TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor)
|
|
|
|
self.insertSubview(self.innerIconView, at: 0)
|
|
|
|
self.disablesInteractiveTransitionGestureRecognizer = true
|
|
|
|
self.updateMode(mode: self.mode, animated: false, force: true)
|
|
|
|
self.delegate = self
|
|
self.isExclusiveTouch = false;
|
|
|
|
self.centerOffset = CGPoint(x: 0.0, y: -1.0 + UIScreenPixel)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool) {
|
|
self.updateMode(mode: mode, animated: animated, force: false)
|
|
}
|
|
|
|
private func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool, force: Bool) {
|
|
if mode != self.mode || force {
|
|
self.mode = mode
|
|
|
|
if animated {
|
|
let previousView = UIImageView(image: self.innerIconView.image)
|
|
previousView.frame = self.innerIconView.frame
|
|
self.addSubview(previousView)
|
|
previousView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
|
previousView.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false, completion: { [weak previousView] _ in
|
|
previousView?.removeFromSuperview()
|
|
})
|
|
}
|
|
|
|
switch self.mode {
|
|
case .audio:
|
|
self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(self.theme)
|
|
self.innerIconView.image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme)
|
|
case .video:
|
|
self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(self.theme)
|
|
self.innerIconView.image = PresentationResourcesChat.chatInputPanelVideoButtonImage(self.theme)
|
|
}
|
|
if let image = self.innerIconView.image {
|
|
let size = self.bounds.size
|
|
let iconSize = image.size
|
|
self.innerIconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
|
}
|
|
|
|
if animated {
|
|
self.innerIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, removeOnCompletion: false)
|
|
self.innerIconView.layer.animateSpring(from: 0.4 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateTheme(theme: PresentationTheme) {
|
|
self.theme = theme
|
|
|
|
switch self.mode {
|
|
case .audio:
|
|
self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(self.theme)
|
|
self.innerIconView.image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme)
|
|
case .video:
|
|
self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(self.theme)
|
|
self.innerIconView.image = PresentationResourcesChat.chatInputPanelVideoButtonImage(self.theme)
|
|
}
|
|
|
|
let inputPanelTheme = theme.chat.inputPanel
|
|
|
|
self.pallete = TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: inputPanelTheme.panelBackgroundColor, borderColor: inputPanelTheme.panelStrokeColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor)
|
|
}
|
|
|
|
deinit {
|
|
if let micLevelDisposable = self.micLevelDisposable {
|
|
micLevelDisposable.dispose()
|
|
}
|
|
if let recordingOverlay = self.recordingOverlay {
|
|
recordingOverlay.dismiss()
|
|
}
|
|
}
|
|
|
|
func cancelRecording() {
|
|
self.isEnabled = false
|
|
self.isEnabled = true
|
|
}
|
|
|
|
func micButtonInteractionBegan() {
|
|
if self.fadeDisabled {
|
|
self.recordingDisabled()
|
|
} else {
|
|
self.modeTimeoutTimer?.invalidate()
|
|
let modeTimeoutTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.modeTimeoutTimer = nil
|
|
strongSelf.beginRecording()
|
|
}
|
|
}, queue: Queue.mainQueue())
|
|
self.modeTimeoutTimer = modeTimeoutTimer
|
|
modeTimeoutTimer.start()
|
|
}
|
|
}
|
|
|
|
func micButtonInteractionCancelled(_ velocity: CGPoint) {
|
|
self.modeTimeoutTimer?.invalidate()
|
|
self.endRecording(false)
|
|
}
|
|
|
|
func micButtonInteractionCompleted(_ velocity: CGPoint) {
|
|
if let modeTimeoutTimer = self.modeTimeoutTimer {
|
|
modeTimeoutTimer.invalidate()
|
|
self.modeTimeoutTimer = nil
|
|
self.switchMode()
|
|
}
|
|
self.endRecording(true)
|
|
}
|
|
|
|
func micButtonInteractionUpdate(_ offset: CGPoint) {
|
|
self.controlsOffset = offset.x
|
|
self.offsetRecordingControls()
|
|
}
|
|
|
|
func micButtonInteractionLocked() {
|
|
self.updateLocked(true)
|
|
}
|
|
|
|
func micButtonInteractionRequestedLockedAction() {
|
|
}
|
|
|
|
func micButtonInteractionStopped() {
|
|
self.stopRecording()
|
|
}
|
|
|
|
func micButtonShouldLock() -> Bool {
|
|
return true
|
|
}
|
|
|
|
func micButtonPresenter() -> TGModernConversationInputMicButtonPresentation! {
|
|
return ChatTextInputMediaRecordingButtonPresenter(account: self.account!, presentController: self.presentController)
|
|
}
|
|
|
|
private var previousSize = CGSize()
|
|
func layoutItems() {
|
|
let size = self.bounds.size
|
|
if size != self.previousSize {
|
|
self.previousSize = size
|
|
let iconSize = self.innerIconView.bounds.size
|
|
self.innerIconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
|
}
|
|
}
|
|
}
|