diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index 60b1bd5df2..839f28e184 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -59,6 +59,7 @@ public final class DeviceLocationManager: NSObject { self.manager.distanceFilter = 5.0 self.manager.activityType = .other self.manager.pausesLocationUpdatesAutomatically = false + self.manager.headingFilter = 2.0 } public func push(mode: DeviceLocationMode, updated: @escaping (CLLocation, Double?) -> Void) -> Disposable { @@ -119,6 +120,19 @@ public final class DeviceLocationManager: NSObject { } } +extension CLHeading { + var effectiveHeading: Double? { + if self.headingAccuracy < 0.0 { + return nil + } + if self.trueHeading > 0.0 { + return self.trueHeading + } else { + return self.magneticHeading + } + } +} + extension DeviceLocationManager: CLLocationManagerDelegate { public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { assert(self.queue.isCurrent()) @@ -127,7 +141,7 @@ extension DeviceLocationManager: CLLocationManagerDelegate { if self.currentTopMode != nil { self.currentLocation = location for subscriber in self.subscribers { - subscriber.update(location, self.currentHeading?.magneticHeading) + subscriber.update(location, self.currentHeading?.effectiveHeading) } } } @@ -140,7 +154,7 @@ extension DeviceLocationManager: CLLocationManagerDelegate { self.currentHeading = newHeading if let currentLocation = self.currentLocation { for subscriber in self.subscribers { - subscriber.update(currentLocation, newHeading.magneticHeading) + subscriber.update(currentLocation, newHeading.effectiveHeading) } } } diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 815b56521d..e9281d4c44 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -355,6 +355,8 @@ open class NavigationController: UINavigationController, ContainableController, } } + let initialPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden() + var overlayLayout = layout if let globalOverlayContainerParent = self.globalOverlayContainerParent { @@ -988,10 +990,15 @@ open class NavigationController: UINavigationController, ContainableController, self.isUpdatingContainers = false if notifyGlobalOverlayControllersUpdated { - self.globalOverlayControllersUpdated?() + self.internalGlobalOverlayControllersUpdated() } self.updateSupportedOrientations?() + + let updatedPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden() + if initialPrefersOnScreenNavigationHidden != updatedPrefersOnScreenNavigationHidden { + self.currentWindow?.invalidatePrefersOnScreenNavigationHidden() + } } private func controllerRemoved(_ controller: ViewController) { @@ -1184,7 +1191,7 @@ open class NavigationController: UINavigationController, ContainableController, if overlayContainer.controller === controller { overlayContainer.removeFromSupernode() strongSelf.globalOverlayContainers.remove(at: i) - strongSelf.globalOverlayControllersUpdated?() + strongSelf.internalGlobalOverlayControllersUpdated() break } } @@ -1194,6 +1201,7 @@ open class NavigationController: UINavigationController, ContainableController, if overlayContainer.controller === controller { overlayContainer.removeFromSupernode() strongSelf.overlayContainers.remove(at: i) + strongSelf.internalOverlayControllersUpdated() break } } @@ -1395,4 +1403,21 @@ open class NavigationController: UINavigationController, ContainableController, } } } + + private func internalGlobalOverlayControllersUpdated() { + self.globalOverlayControllersUpdated?() + self.currentWindow?.invalidatePrefersOnScreenNavigationHidden() + } + + private func internalOverlayControllersUpdated() { + self.currentWindow?.invalidatePrefersOnScreenNavigationHidden() + } + + private func collectPrefersOnScreenNavigationHidden() -> Bool { + var hidden = false + if let overlayController = self.topOverlayController { + hidden = hidden || overlayController.prefersOnScreenNavigationHidden + } + return hidden + } } diff --git a/submodules/LiveLocationPositionNode/Sources/ChatMessageLiveLocationPositionNode.swift b/submodules/LiveLocationPositionNode/Sources/ChatMessageLiveLocationPositionNode.swift index 2d6429d89e..4c671c379f 100644 --- a/submodules/LiveLocationPositionNode/Sources/ChatMessageLiveLocationPositionNode.swift +++ b/submodules/LiveLocationPositionNode/Sources/ChatMessageLiveLocationPositionNode.swift @@ -147,12 +147,17 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode { var hasPulse = false var heading: Double? var coordinate: (Double, Double)? + + func degToRad(_ degrees: Double) -> Double { + return degrees * Double.pi / 180.0 + } + switch mode { case let .liveLocation(_, active, latitude, longitude, headingValue): backgroundImage = avatarBackgroundImage hasPulse = active coordinate = (latitude, longitude) - heading = headingValue.flatMap { Double($0) } + heading = headingValue.flatMap { degToRad(Double($0)) } case let .location(location): let venueType = location?.venue?.type ?? "" let color = venueType.isEmpty ? theme.list.itemAccentColor : venueIconColor(type: venueType) @@ -162,10 +167,6 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode { } } - func degToRad(_ degrees: Double) -> Double { - return degrees * Double.pi / 180.0 - } - if heading == nil, let currentCoordinate = currentCoordinate, let coordinate = coordinate { let lat1 = degToRad(currentCoordinate.0) let lon1 = degToRad(currentCoordinate.1) @@ -248,7 +249,7 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode { strongSelf.arrowNode.isHidden = heading == nil || !hasPulse strongSelf.arrowNode.position = CGPoint(x: 31.0, y: 64.0) - strongSelf.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading ?? 0.0 / 180.0 * Double.pi), 0.0, 0.0, 1.0) + strongSelf.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading ?? 0), 0.0, 0.0, 1.0) } }) } diff --git a/submodules/LocationUI/Sources/LocationAnnotation.swift b/submodules/LocationUI/Sources/LocationAnnotation.swift index 0a66824904..5a7cffa715 100644 --- a/submodules/LocationUI/Sources/LocationAnnotation.swift +++ b/submodules/LocationUI/Sources/LocationAnnotation.swift @@ -43,7 +43,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation { let peer: Peer? let message: Message? let forcedSelection: Bool - var heading: Int32? { + @objc dynamic var heading: NSNumber? { willSet { self.willChangeValue(forKey: "heading") } @@ -91,7 +91,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation { } self.selfPeer = selfPeer self.forcedSelection = false - self.heading = heading + self.heading = heading.flatMap { NSNumber(value: $0) } super.init() } @@ -167,6 +167,8 @@ class LocationPinAnnotationView: MKAnnotationView { var hasPulse = false + var headingKvoToken: NSKeyValueObservation? + override class var layerClass: AnyClass { return LocationPinAnnotationLayer.self } @@ -233,6 +235,14 @@ class LocationPinAnnotationView: MKAnnotationView { self.annotation = annotation } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.headingKvoToken?.invalidate() + } + var defaultZPosition: CGFloat { if let annotation = self.annotation as? LocationPinAnnotation { if annotation.forcedSelection { @@ -247,10 +257,6 @@ class LocationPinAnnotationView: MKAnnotationView { } } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - override var annotation: MKAnnotation? { didSet { if let annotation = self.annotation as? LocationPinAnnotation { @@ -270,6 +276,18 @@ class LocationPinAnnotationView: MKAnnotationView { self.shadowNode.isHidden = true self.smallNode.isHidden = false } + + if let headingKvoToken = self.headingKvoToken { + self.headingKvoToken = nil + headingKvoToken.invalidate() + } + + self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, change) in + guard let heading = change.newValue else { + return + } + self?.updateHeading(heading) + } } else if let peer = annotation.peer { self.iconNode.isHidden = true @@ -278,6 +296,12 @@ class LocationPinAnnotationView: MKAnnotationView { self.setPeer(context: annotation.context, theme: annotation.theme, peer: peer) self.setSelected(true, animated: false) + + if let headingKvoToken = self.headingKvoToken { + self.headingKvoToken = nil + headingKvoToken.invalidate() + } + self.updateHeading(nil) } else if let location = annotation.location { let venueType = location.venue?.type ?? "" let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType) @@ -299,15 +323,36 @@ class LocationPinAnnotationView: MKAnnotationView { self.setSelected(true, animated: false) } + if let avatarNode = self.avatarNode { + self.avatarNode = nil + avatarNode.removeFromSupernode() + } + if self.initialized && !self.appeared { self.appeared = true self.animateAppearance() } + + if let headingKvoToken = self.headingKvoToken { + self.headingKvoToken = nil + headingKvoToken.invalidate() + } + self.updateHeading(nil) } } } } + private func updateHeading(_ heading: NSNumber?) { + if let heading = heading?.int32Value { + self.arrowNode.isHidden = false + self.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0) + } else { + self.arrowNode.isHidden = true + self.arrowNode.transform = CATransform3DIdentity + } + } + override func prepareForReuse() { self.smallNode.isHidden = true self.backgroundNode.isHidden = false diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index 7ec44d33e8..114df8d0b7 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -372,6 +372,11 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { var trackingMode: LocationTrackingMode = .none { didSet { self.mapView?.userTrackingMode = self.trackingMode.userTrackingMode + if self.trackingMode == .followWithHeading && self.headingArrowView?.image != nil { + self.headingArrowView?.image = nil + } else if self.trackingMode != .followWithHeading && self.headingArrowView?.image == nil { + self.headingArrowView?.image = generateHeadingArrowImage() + } } } @@ -676,9 +681,45 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate { } if let updatedAnnotation = dict[annotation.id] { + func degToRad(_ degrees: Double) -> Double { + return degrees * Double.pi / 180.0 + } + + func radToDeg(_ radians: Double) -> Double { + return radians / Double.pi * 180.0 + } + + let currentCoordinate = annotation.coordinate + let coordinate = updatedAnnotation.coordinate + var heading = updatedAnnotation.heading + if heading == nil { + let previous = CLLocation(latitude: currentCoordinate.latitude, longitude: currentCoordinate.longitude) + let new = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + + if new.distance(from: previous) > 10 { + let lat1 = degToRad(currentCoordinate.latitude) + let lon1 = degToRad(currentCoordinate.longitude) + let lat2 = degToRad(coordinate.latitude) + let lon2 = degToRad(coordinate.longitude) + + let dLat = lat2 - lat1 + let dLon = lon2 - lon1 + + if dLat != 0 && dLon != 0 { + let y = sin(dLon) * cos(lat2) + let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon) + heading = NSNumber(value: radToDeg(atan2(y, x))) + } + } else { + heading = annotation.heading + } + } + UIView.animate(withDuration: 0.2) { annotation.coordinate = updatedAnnotation.coordinate } + + annotation.heading = heading dict[annotation.id] = nil } else { annotationsToRemove.insert(annotation) diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 1968bfa591..6c063fccc9 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -544,7 +544,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { - self.headerNode.mapNode.userHeading = CGFloat(newHeading.magneticHeading) + if newHeading.headingAccuracy < 0.0 { + self.headerNode.mapNode.userHeading = nil + } + if newHeading.trueHeading > 0.0 { + self.headerNode.mapNode.userHeading = CGFloat(newHeading.trueHeading) + } else { + self.headerNode.mapNode.userHeading = CGFloat(newHeading.magneticHeading) + } } func updatePresentationData(_ presentationData: PresentationData) { diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index 07e3ebf9a7..759466aba7 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -111,6 +111,7 @@ private final class MediaPlayerContext { private var baseRate: Double private let fetchAutomatically: Bool private var playAndRecord: Bool + private var ambient: Bool private var keepAudioSessionWhilePaused: Bool private var continuePlayingWithoutSoundOnLostAudioSession: Bool @@ -132,7 +133,7 @@ private final class MediaPlayerContext { private var stoppedAtEnd = false - init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, ambient: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool) { assert(queue.isCurrent()) self.queue = queue @@ -149,6 +150,7 @@ private final class MediaPlayerContext { self.baseRate = baseRate self.fetchAutomatically = fetchAutomatically self.playAndRecord = playAndRecord + self.ambient = ambient self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession @@ -368,7 +370,7 @@ private final class MediaPlayerContext { self.audioRenderer = nil let queue = self.queue - renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in + renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, ambient: self.ambient, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in queue.async { if let strongSelf = self { strongSelf.tick() @@ -446,7 +448,7 @@ private final class MediaPlayerContext { self.lastStatusUpdateTimestamp = nil if self.enableSound { let queue = self.queue - let renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in + let renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), playAndRecord: self.playAndRecord, ambient: self.ambient, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in queue.async { if let strongSelf = self { strongSelf.tick() @@ -998,10 +1000,10 @@ public final class MediaPlayer { } } - public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false) { + public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, ambient: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false) { let audioLevelPipe = self.audioLevelPipe self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, ambient: ambient, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift index 30a50a3ede..e531344f15 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift @@ -239,6 +239,7 @@ private final class AudioPlayerRendererContext { let audioSessionDisposable = MetaDisposable() var audioSessionControl: ManagedAudioSessionControl? let playAndRecord: Bool + let ambient: Bool var forceAudioToSpeaker: Bool { didSet { if self.forceAudioToSpeaker != oldValue { @@ -249,7 +250,7 @@ private final class AudioPlayerRendererContext { } } - init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { + init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, ambient: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { assert(audioPlayerRendererQueue.isCurrent()) self.audioSession = audioSession @@ -262,6 +263,7 @@ private final class AudioPlayerRendererContext { self.audioPaused = audioPaused self.playAndRecord = playAndRecord + self.ambient = ambient self.audioStreamDescription = audioRendererNativeStreamDescription() @@ -481,7 +483,7 @@ private final class AudioPlayerRendererContext { switch self.audioSession { case let .manager(manager): - self.audioSessionDisposable.set(manager.push(audioSessionType: self.playAndRecord ? .playWithPossiblePortOverride : .play, outputMode: self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system, once: true, manualActivate: { [weak self] control in + self.audioSessionDisposable.set(manager.push(audioSessionType: self.ambient ? .ambient : (self.playAndRecord ? .playWithPossiblePortOverride : .play), outputMode: self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system, once: true, manualActivate: { [weak self] control in audioPlayerRendererQueue.async { if let strongSelf = self { strongSelf.audioSessionControl = control @@ -751,7 +753,7 @@ public final class MediaPlayerAudioRenderer { private let audioClock: CMClock public let audioTimebase: CMTimebase - public init(audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { + public init(audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, ambient: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { var audioClock: CMClock? CMAudioClockCreate(allocator: nil, clockOut: &audioClock) if audioClock == nil { @@ -764,7 +766,7 @@ public final class MediaPlayerAudioRenderer { self.audioTimebase = audioTimebase! audioPlayerRendererQueue.async { - let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSession: audioSession, playAndRecord: playAndRecord, forceAudioToSpeaker: forceAudioToSpeaker, baseRate: baseRate, audioLevelPipe: audioLevelPipe, updatedRate: updatedRate, audioPaused: audioPaused) + let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSession: audioSession, playAndRecord: playAndRecord, ambient: ambient, forceAudioToSpeaker: forceAudioToSpeaker, baseRate: baseRate, audioLevelPipe: audioLevelPipe, updatedRate: updatedRate, audioPaused: audioPaused) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index a1fcb3c8b8..232c13101e 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -4,6 +4,7 @@ import AVFoundation import UIKit public enum ManagedAudioSessionType: Equatable { + case ambient case play case playWithPossiblePortOverride case record(speaker: Bool) @@ -22,6 +23,8 @@ public enum ManagedAudioSessionType: Equatable { private func nativeCategoryForType(_ type: ManagedAudioSessionType, headphones: Bool, outputMode: AudioSessionOutputMode) -> AVAudioSession.Category { switch type { + case .ambient: + return .ambient case .play: return .playback case .record, .voiceCall, .videoCall: @@ -665,7 +668,7 @@ public final class ManagedAudioSession { print("ManagedAudioSession setting category for \(type) (native: \(nativeCategory))") var options: AVAudioSession.CategoryOptions = [] switch type { - case .play: + case .play, .ambient: break case .playWithPossiblePortOverride: if case .playAndRecord = nativeCategory { diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index a0b65b282c..3419e7f136 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -34,7 +34,7 @@ private final class PresentationCallToneRenderer { self.toneRenderer = MediaPlayerAudioRenderer(audioSession: .custom({ control in return controlImpl?(control) ?? EmptyDisposable - }), playAndRecord: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: self.audioLevelPipe, updatedRate: {}, audioPaused: {}) + }), playAndRecord: false, ambient: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: self.audioLevelPipe, updatedRate: {}, audioPaused: {}) controlImpl = { [weak self] control in queue.async { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9a4318b5cc..d273438545 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5232,62 +5232,104 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - }, unpinMessage: { [weak self] id, askForConfirmation in - if let strongSelf = self { - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - if strongSelf.canManagePin() { - let action: () -> Void = { - if let strongSelf = self { - let disposable: MetaDisposable - if let current = strongSelf.unpinMessageDisposable { - disposable = current - } else { - disposable = MetaDisposable() - strongSelf.unpinMessageDisposable = disposable - } - - if askForConfirmation { - strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = true - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedPendingUnpinnedAllMessages(true) - }) - + }, unpinMessage: { [weak self] id, askForConfirmation, contextController in + let impl: () -> Void = { + guard let strongSelf = self else { + return + } + guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { + return + } + + if strongSelf.canManagePin() { + let action: () -> Void = { + if let strongSelf = self { + let disposable: MetaDisposable + if let current = strongSelf.unpinMessageDisposable { + disposable = current + } else { + disposable = MetaDisposable() + strongSelf.unpinMessageDisposable = disposable + } + + if askForConfirmation { + strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = true + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedPendingUnpinnedAllMessages(true) + }) + + strongSelf.present( + UndoOverlayController( + presentationData: strongSelf.presentationData, + content: .messagesUnpinned( + title: strongSelf.presentationData.strings.Chat_MessagesUnpinned(1), + text: "", + undo: askForConfirmation, + isHidden: false + ), + elevatedLayout: false, + action: { action in + switch action { + case .commit: + disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)) + |> deliverOnMainQueue).start(error: { _ in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedPendingUnpinnedAllMessages(false) + }) + }, completed: { + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedPendingUnpinnedAllMessages(false) + }) + })) + case .undo: + strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedPendingUnpinnedAllMessages(false) + }) + default: + break + } + return true + } + ), + in: .current + ) + } else { + if case .pinnedMessages = strongSelf.presentationInterfaceState.subject { + strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.insert(id) strongSelf.present( UndoOverlayController( presentationData: strongSelf.presentationData, content: .messagesUnpinned( title: strongSelf.presentationData.strings.Chat_MessagesUnpinned(1), text: "", - undo: askForConfirmation, + undo: true, isHidden: false ), elevatedLayout: false, action: { action in + guard let strongSelf = self else { + return true + } switch action { case .commit: - disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)) - |> deliverOnMainQueue).start(error: { _ in + let _ = (requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)) + |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return } - strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedPendingUnpinnedAllMessages(false) - }) - }, completed: { - guard let strongSelf = self else { - return - } - strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedPendingUnpinnedAllMessages(false) - }) - })) - case .undo: - strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedPendingUnpinnedAllMessages(false) + strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.remove(id) }) + case .undo: + strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.remove(id) default: break } @@ -5302,62 +5344,70 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - if askForConfirmation { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: { - action() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) - } else { + } + if askForConfirmation { + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_Unpin, action: { action() - } + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) } else { - if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage { - let previousClosedPinnedMessageId = strongSelf.presentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in - var value = value - value.closedPinnedMessageId = pinnedMessage.topMessageId - return value - }) }) - }) - strongSelf.present( - UndoOverlayController( - presentationData: strongSelf.presentationData, - content: .messagesUnpinned( - title: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenTitle, - text: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenText, - undo: true, - isHidden: false - ), - elevatedLayout: false, - action: { action in - guard let strongSelf = self else { - return true - } - switch action { - case .commit: - break - case .undo: - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in - var value = value - value.closedPinnedMessageId = previousClosedPinnedMessageId - return value - }) }) - }) - default: - break - } + action() + } + } else { + if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage { + let previousClosedPinnedMessageId = strongSelf.presentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in + var value = value + value.closedPinnedMessageId = pinnedMessage.topMessageId + return value + }) }) + }) + strongSelf.present( + UndoOverlayController( + presentationData: strongSelf.presentationData, + content: .messagesUnpinned( + title: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenTitle, + text: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenText, + undo: true, + isHidden: false + ), + elevatedLayout: false, + action: { action in + guard let strongSelf = self else { return true } - ), - in: .current - ) - strongSelf.updatedClosedPinnedMessageId?(pinnedMessage.topMessageId) - } + switch action { + case .commit: + break + case .undo: + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in + var value = value + value.closedPinnedMessageId = previousClosedPinnedMessageId + return value + }) }) + }) + default: + break + } + return true + } + ), + in: .current + ) + strongSelf.updatedClosedPinnedMessageId?(pinnedMessage.topMessageId) } } } + + if let contextController = contextController { + contextController.dismiss(completion: { + impl() + }) + } else { + impl() + } }, unpinAllMessages: { [weak self] in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 52274d6575..4d408b82f5 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -8,7 +8,7 @@ import AccountContext import TelegramPresentationData -func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, pendingUnpinnedAllMessages: Bool, associatedData: ChatMessageItemAssociatedData, updatingMedia: [MessageId: ChatUpdatingMessageMedia], customChannelDiscussionReadState: MessageId?, customThreadOutgoingReadState: MessageId?) -> [ChatHistoryEntry] { +func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, pendingUnpinnedAllMessages: Bool, pendingRemovedMessages: Set, associatedData: ChatMessageItemAssociatedData, updatingMedia: [MessageId: ChatUpdatingMessageMedia], customChannelDiscussionReadState: MessageId?, customThreadOutgoingReadState: MessageId?) -> [ChatHistoryEntry] { if historyAppearsCleared { return [] } @@ -34,6 +34,10 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, var message = entry.message var isRead = entry.isRead + if pendingRemovedMessages.contains(message.id) { + continue + } + if let customThreadOutgoingReadState = customThreadOutgoingReadState { isRead = customThreadOutgoingReadState >= message.id } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 3d8c2f6446..b78bbf8691 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -565,6 +565,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } + private let pendingRemovedMessagesPromise = ValuePromise>(Set()) + var pendingRemovedMessages: Set = Set() { + didSet { + if self.pendingRemovedMessages != oldValue { + self.pendingRemovedMessagesPromise.set(self.pendingRemovedMessages) + } + } + } + private(set) var isScrollAtBottomPosition = false public var isScrollAtBottomPositionUpdated: (() -> Void)? @@ -846,10 +855,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { automaticDownloadNetworkType, self.historyAppearsClearedPromise.get(), self.pendingUnpinnedAllMessagesPromise.get(), + self.pendingRemovedMessagesPromise.get(), animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState - ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState in + ).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState in func applyHole() { Queue.mainQueue().async { if let strongSelf = self { @@ -930,7 +940,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, subject: subject) - let filteredEntries = chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, associatedData: associatedData, updatingMedia: updatingMedia, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState) + let filteredEntries = chatHistoryEntriesForView(location: chatLocation, view: view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles && tagMask == nil, includeChatInfoEntry: mode == .bubbles, includeSearchEntry: includeSearchEntry && tagMask != nil, reverse: reverse, groupMessages: mode == .bubbles, selectedMessages: selectedMessages, presentationData: chatPresentationData, historyAppearsCleared: historyAppearsCleared, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, pendingRemovedMessages: pendingRemovedMessages, associatedData: associatedData, updatingMedia: updatingMedia, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState) let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0 let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id) let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages)) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index a68301a3a3..8d6590dca3 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -687,9 +687,8 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if let pinnedSelectedMessageId = pinnedSelectedMessageId { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Unpin, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - interfaceInteraction.unpinMessage(pinnedSelectedMessageId, false) - f(.default) + }, action: { c, _ in + interfaceInteraction.unpinMessage(pinnedSelectedMessageId, false, c) }))) } else { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Pin, icon: { theme in diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 8af7550cab..19b3198017 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -176,6 +176,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var highlightedState: Bool = false private var haptic: EmojiHaptic? + private var mediaPlayer: MediaPlayer? + private let mediaStatusDisposable = MetaDisposable() private var currentSwipeToReplyTranslation: CGFloat = 0.0 @@ -246,6 +248,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { deinit { self.disposable.dispose() + self.mediaStatusDisposable.set(nil) } required init?(coder aDecoder: NSCoder) { @@ -1231,6 +1234,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let beatingHearts: [UInt32] = [0x2764, 0x1F90E, 0x1F9E1, 0x1F499, 0x1F49A, 0x1F49C, 0x1F49B, 0x1F5A4, 0x1F90D] let peach = 0x1F351 + let coffin = 0x26B0 let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> take(1) @@ -1271,16 +1275,46 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if shouldPlay { let _ = (appConfiguration |> deliverOnMainQueue).start(next: { [weak self] appConfiguration in + guard let strongSelf = self else { + return + } let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: item.context.account) for (emoji, file) in emojiSounds.sounds { if emoji.unicodeScalars.first == firstScalar { let mediaManager = item.context.sharedContext.mediaManager - let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true) + let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true) mediaPlayer.togglePlayPause() - self?.mediaPlayer = mediaPlayer - - animationNode.play() + mediaPlayer.actionAtEnd = .action({ [weak self] in + self?.mediaPlayer = nil + }) + strongSelf.mediaPlayer = mediaPlayer + strongSelf.mediaStatusDisposable.set((mediaPlayer.status + |> deliverOnMainQueue).start(next: { [weak self, weak animationNode] status in + if let strongSelf = self { + if firstScalar.value == coffin { + var haptic: EmojiHaptic + if let current = strongSelf.haptic { + haptic = current + } else { + haptic = CoffinHaptic() + haptic.enabled = true + strongSelf.haptic = haptic + } + if !haptic.active { + haptic.start(time: 0.0) + } + } + + switch status.status { + case .playing: + animationNode?.play() + strongSelf.mediaStatusDisposable.set(nil) + default: + break + } + } + })) break } } @@ -1303,8 +1337,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { return nil } - private var mediaPlayer: MediaPlayer? - @objc private func shareButtonPressed() { if let item = self.item { if case .pinnedMessages = item.associatedData.subject { diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 7550744ef6..6e15f77894 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -544,33 +544,34 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - if let subnodes = self.subnodes { - for node in subnodes { - if let contextNode = node as? ContextExtractedContentContainingNode { - if let contextSubnodes = contextNode.contentNode.subnodes { - inner: for contextSubnode in contextSubnodes { - if contextSubnode !== self.accessoryItemNode { - if contextSubnode == self.backgroundNode { - if self.backgroundNode.hasImage && self.backgroundWallpaperNode.hasImage { - continue inner - } - } - contextSubnode.layer.allowsGroupOpacity = true - contextSubnode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak contextSubnode] _ in - contextSubnode?.layer.allowsGroupOpacity = false - }) - } - } - } - } else if node !== self.accessoryItemNode { + func process(node: ASDisplayNode) { + if node === self.accessoryItemNode { + return + } + + if node !== self { + switch node { + case _ as ContextExtractedContentContainingNode, _ as ContextControllerSourceNode, _ as ContextExtractedContentNode: + break + default: node.layer.allowsGroupOpacity = true - node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak node] _ in node?.layer.allowsGroupOpacity = false }) + return } } + + guard let subnodes = node.subnodes else { + return + } + + for subnode in subnodes { + process(node: subnode) + } } + + process(node: self) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { @@ -588,10 +589,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode override func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) - self.allowsGroupOpacity = true - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in - self?.allowsGroupOpacity = false - }) + if let subnodes = self.subnodes { + for subnode in subnodes { + let layer = subnode.layer + layer.allowsGroupOpacity = true + layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak layer] _ in + layer?.allowsGroupOpacity = false + }) + } + } } override func didLoad() { diff --git a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift index 829ab1145f..5b7b604d99 100644 --- a/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatPanelInterfaceInteraction.swift @@ -95,7 +95,7 @@ final class ChatPanelInterfaceInteraction { let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool let unblockPeer: () -> Void let pinMessage: (MessageId, ContextController?) -> Void - let unpinMessage: (MessageId, Bool) -> Void + let unpinMessage: (MessageId, Bool, ContextController?) -> Void let unpinAllMessages: () -> Void let openPinnedList: (MessageId) -> Void let shareAccountContact: () -> Void @@ -174,7 +174,7 @@ final class ChatPanelInterfaceInteraction { sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId, ContextController?) -> Void, - unpinMessage: @escaping (MessageId, Bool) -> Void, + unpinMessage: @escaping (MessageId, Bool, ContextController?) -> Void, unpinAllMessages: @escaping () -> Void, openPinnedList: @escaping (MessageId) -> Void, shareAccountContact: @escaping () -> Void, diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 0561245ff2..a6bab54813 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -462,7 +462,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { @objc func closePressed() { if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage { - interfaceInteraction.unpinMessage(message.message.id, true) + interfaceInteraction.unpinMessage(message.message.id, true, nil) } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index f92dab1d5b..b350a848f9 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -100,7 +100,7 @@ final class ChatRecentActionsController: TelegramBaseController { return false }, unblockPeer: { }, pinMessage: { _, _ in - }, unpinMessage: { _, _ in + }, unpinMessage: { _, _, _ in }, unpinAllMessages: { }, openPinnedList: { _ in }, shareAccountContact: { diff --git a/submodules/TelegramUI/Sources/CoffinHaptic.swift b/submodules/TelegramUI/Sources/CoffinHaptic.swift new file mode 100644 index 0000000000..b1e0713ec3 --- /dev/null +++ b/submodules/TelegramUI/Sources/CoffinHaptic.swift @@ -0,0 +1,74 @@ +import Foundation +import Display +import SwiftSignalKit + +private let firstImpactTime: Double = 0.4 +private let secondImpactTime: Double = 0.6 + +final class CoffinHaptic: EmojiHaptic { + private var hapticFeedback = HapticFeedback() + private var timer: SwiftSignalKit.Timer? + private var time: Double = 0.0 + var enabled: Bool = false { + didSet { + if !self.enabled { + self.reset() + } + } + } + + var active: Bool { + return self.timer != nil + } + + private func reset() { + if let timer = self.timer { + self.time = 0.0 + timer.invalidate() + self.timer = nil + } + } + + private func beat(time: Double) { + let epsilon = 0.1 + if fabs(firstImpactTime - time) < epsilon || fabs(secondImpactTime - time) < epsilon { + self.hapticFeedback.impact(.heavy) + } + } + + func start(time: Double) { + self.hapticFeedback.prepareImpact() + + if time > firstImpactTime { + return + } + + let startTime: Double = 0.0 + + let block = { [weak self] in + guard let strongSelf = self, strongSelf.enabled else { + return + } + + strongSelf.time = startTime + strongSelf.beat(time: startTime) + strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in + guard let strongSelf = self, strongSelf.enabled else { + return + } + strongSelf.time += 0.2 + strongSelf.beat(time: strongSelf.time) + + if strongSelf.time > secondImpactTime { + strongSelf.reset() + strongSelf.time = 0.0 + strongSelf.timer?.invalidate() + strongSelf.timer = nil + } + }, queue: Queue.mainQueue()) + strongSelf.timer?.start() + } + + block() + } +} diff --git a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift index a01bc652a3..58c481129a 100644 --- a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift +++ b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift @@ -214,7 +214,7 @@ final class ManagedAudioRecorderContext { } return ActionDisposable { } - }), playAndRecord: true, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe(), updatedRate: { + }), playAndRecord: true, ambient: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe(), updatedRate: { }, audioPaused: {}) self.toneRenderer = toneRenderer diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index e7f55c474e..ab94f05d5f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -406,7 +406,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { return false }, unblockPeer: { }, pinMessage: { _, _ in - }, unpinMessage: { _, _ in + }, unpinMessage: { _, _, _ in }, unpinAllMessages: { }, openPinnedList: { _ in }, shareAccountContact: {