Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
overtake 2020-10-27 23:18:20 +04:00
commit 36784fab3d
22 changed files with 474 additions and 159 deletions

View File

@ -59,6 +59,7 @@ public final class DeviceLocationManager: NSObject {
self.manager.distanceFilter = 5.0 self.manager.distanceFilter = 5.0
self.manager.activityType = .other self.manager.activityType = .other
self.manager.pausesLocationUpdatesAutomatically = false self.manager.pausesLocationUpdatesAutomatically = false
self.manager.headingFilter = 2.0
} }
public func push(mode: DeviceLocationMode, updated: @escaping (CLLocation, Double?) -> Void) -> Disposable { 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 { extension DeviceLocationManager: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
assert(self.queue.isCurrent()) assert(self.queue.isCurrent())
@ -127,7 +141,7 @@ extension DeviceLocationManager: CLLocationManagerDelegate {
if self.currentTopMode != nil { if self.currentTopMode != nil {
self.currentLocation = location self.currentLocation = location
for subscriber in self.subscribers { 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 self.currentHeading = newHeading
if let currentLocation = self.currentLocation { if let currentLocation = self.currentLocation {
for subscriber in self.subscribers { for subscriber in self.subscribers {
subscriber.update(currentLocation, newHeading.magneticHeading) subscriber.update(currentLocation, newHeading.effectiveHeading)
} }
} }
} }

View File

@ -355,6 +355,8 @@ open class NavigationController: UINavigationController, ContainableController,
} }
} }
let initialPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden()
var overlayLayout = layout var overlayLayout = layout
if let globalOverlayContainerParent = self.globalOverlayContainerParent { if let globalOverlayContainerParent = self.globalOverlayContainerParent {
@ -988,10 +990,15 @@ open class NavigationController: UINavigationController, ContainableController,
self.isUpdatingContainers = false self.isUpdatingContainers = false
if notifyGlobalOverlayControllersUpdated { if notifyGlobalOverlayControllersUpdated {
self.globalOverlayControllersUpdated?() self.internalGlobalOverlayControllersUpdated()
} }
self.updateSupportedOrientations?() self.updateSupportedOrientations?()
let updatedPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden()
if initialPrefersOnScreenNavigationHidden != updatedPrefersOnScreenNavigationHidden {
self.currentWindow?.invalidatePrefersOnScreenNavigationHidden()
}
} }
private func controllerRemoved(_ controller: ViewController) { private func controllerRemoved(_ controller: ViewController) {
@ -1184,7 +1191,7 @@ open class NavigationController: UINavigationController, ContainableController,
if overlayContainer.controller === controller { if overlayContainer.controller === controller {
overlayContainer.removeFromSupernode() overlayContainer.removeFromSupernode()
strongSelf.globalOverlayContainers.remove(at: i) strongSelf.globalOverlayContainers.remove(at: i)
strongSelf.globalOverlayControllersUpdated?() strongSelf.internalGlobalOverlayControllersUpdated()
break break
} }
} }
@ -1194,6 +1201,7 @@ open class NavigationController: UINavigationController, ContainableController,
if overlayContainer.controller === controller { if overlayContainer.controller === controller {
overlayContainer.removeFromSupernode() overlayContainer.removeFromSupernode()
strongSelf.overlayContainers.remove(at: i) strongSelf.overlayContainers.remove(at: i)
strongSelf.internalOverlayControllersUpdated()
break 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
}
} }

View File

@ -147,12 +147,17 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode {
var hasPulse = false var hasPulse = false
var heading: Double? var heading: Double?
var coordinate: (Double, Double)? var coordinate: (Double, Double)?
func degToRad(_ degrees: Double) -> Double {
return degrees * Double.pi / 180.0
}
switch mode { switch mode {
case let .liveLocation(_, active, latitude, longitude, headingValue): case let .liveLocation(_, active, latitude, longitude, headingValue):
backgroundImage = avatarBackgroundImage backgroundImage = avatarBackgroundImage
hasPulse = active hasPulse = active
coordinate = (latitude, longitude) coordinate = (latitude, longitude)
heading = headingValue.flatMap { Double($0) } heading = headingValue.flatMap { degToRad(Double($0)) }
case let .location(location): case let .location(location):
let venueType = location?.venue?.type ?? "" let venueType = location?.venue?.type ?? ""
let color = venueType.isEmpty ? theme.list.itemAccentColor : venueIconColor(type: venueType) 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 { if heading == nil, let currentCoordinate = currentCoordinate, let coordinate = coordinate {
let lat1 = degToRad(currentCoordinate.0) let lat1 = degToRad(currentCoordinate.0)
let lon1 = degToRad(currentCoordinate.1) let lon1 = degToRad(currentCoordinate.1)
@ -248,7 +249,7 @@ public final class ChatMessageLiveLocationPositionNode: ASDisplayNode {
strongSelf.arrowNode.isHidden = heading == nil || !hasPulse strongSelf.arrowNode.isHidden = heading == nil || !hasPulse
strongSelf.arrowNode.position = CGPoint(x: 31.0, y: 64.0) 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)
} }
}) })
} }

View File

@ -43,7 +43,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
let peer: Peer? let peer: Peer?
let message: Message? let message: Message?
let forcedSelection: Bool let forcedSelection: Bool
var heading: Int32? { @objc dynamic var heading: NSNumber? {
willSet { willSet {
self.willChangeValue(forKey: "heading") self.willChangeValue(forKey: "heading")
} }
@ -91,7 +91,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
} }
self.selfPeer = selfPeer self.selfPeer = selfPeer
self.forcedSelection = false self.forcedSelection = false
self.heading = heading self.heading = heading.flatMap { NSNumber(value: $0) }
super.init() super.init()
} }
@ -167,6 +167,8 @@ class LocationPinAnnotationView: MKAnnotationView {
var hasPulse = false var hasPulse = false
var headingKvoToken: NSKeyValueObservation?
override class var layerClass: AnyClass { override class var layerClass: AnyClass {
return LocationPinAnnotationLayer.self return LocationPinAnnotationLayer.self
} }
@ -233,6 +235,14 @@ class LocationPinAnnotationView: MKAnnotationView {
self.annotation = annotation self.annotation = annotation
} }
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.headingKvoToken?.invalidate()
}
var defaultZPosition: CGFloat { var defaultZPosition: CGFloat {
if let annotation = self.annotation as? LocationPinAnnotation { if let annotation = self.annotation as? LocationPinAnnotation {
if annotation.forcedSelection { 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? { override var annotation: MKAnnotation? {
didSet { didSet {
if let annotation = self.annotation as? LocationPinAnnotation { if let annotation = self.annotation as? LocationPinAnnotation {
@ -270,6 +276,18 @@ class LocationPinAnnotationView: MKAnnotationView {
self.shadowNode.isHidden = true self.shadowNode.isHidden = true
self.smallNode.isHidden = false 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 { else if let peer = annotation.peer {
self.iconNode.isHidden = true self.iconNode.isHidden = true
@ -278,6 +296,12 @@ class LocationPinAnnotationView: MKAnnotationView {
self.setPeer(context: annotation.context, theme: annotation.theme, peer: peer) self.setPeer(context: annotation.context, theme: annotation.theme, peer: peer)
self.setSelected(true, animated: false) self.setSelected(true, animated: false)
if let headingKvoToken = self.headingKvoToken {
self.headingKvoToken = nil
headingKvoToken.invalidate()
}
self.updateHeading(nil)
} else if let location = annotation.location { } else if let location = annotation.location {
let venueType = location.venue?.type ?? "" let venueType = location.venue?.type ?? ""
let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType) let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType)
@ -299,15 +323,36 @@ class LocationPinAnnotationView: MKAnnotationView {
self.setSelected(true, animated: false) self.setSelected(true, animated: false)
} }
if let avatarNode = self.avatarNode {
self.avatarNode = nil
avatarNode.removeFromSupernode()
}
if self.initialized && !self.appeared { if self.initialized && !self.appeared {
self.appeared = true self.appeared = true
self.animateAppearance() 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() { override func prepareForReuse() {
self.smallNode.isHidden = true self.smallNode.isHidden = true
self.backgroundNode.isHidden = false self.backgroundNode.isHidden = false

View File

@ -372,6 +372,11 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
var trackingMode: LocationTrackingMode = .none { var trackingMode: LocationTrackingMode = .none {
didSet { didSet {
self.mapView?.userTrackingMode = self.trackingMode.userTrackingMode 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] { 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) { UIView.animate(withDuration: 0.2) {
annotation.coordinate = updatedAnnotation.coordinate annotation.coordinate = updatedAnnotation.coordinate
} }
annotation.heading = heading
dict[annotation.id] = nil dict[annotation.id] = nil
} else { } else {
annotationsToRemove.insert(annotation) annotationsToRemove.insert(annotation)

View File

@ -544,7 +544,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
} }
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { 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) { func updatePresentationData(_ presentationData: PresentationData) {

View File

@ -111,6 +111,7 @@ private final class MediaPlayerContext {
private var baseRate: Double private var baseRate: Double
private let fetchAutomatically: Bool private let fetchAutomatically: Bool
private var playAndRecord: Bool private var playAndRecord: Bool
private var ambient: Bool
private var keepAudioSessionWhilePaused: Bool private var keepAudioSessionWhilePaused: Bool
private var continuePlayingWithoutSoundOnLostAudioSession: Bool private var continuePlayingWithoutSoundOnLostAudioSession: Bool
@ -132,7 +133,7 @@ private final class MediaPlayerContext {
private var stoppedAtEnd = false private var stoppedAtEnd = false
init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise<MediaPlayerStatus>, audioLevelPipe: ValuePipe<Float>, 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<MediaPlayerStatus>, audioLevelPipe: ValuePipe<Float>, 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()) assert(queue.isCurrent())
self.queue = queue self.queue = queue
@ -149,6 +150,7 @@ private final class MediaPlayerContext {
self.baseRate = baseRate self.baseRate = baseRate
self.fetchAutomatically = fetchAutomatically self.fetchAutomatically = fetchAutomatically
self.playAndRecord = playAndRecord self.playAndRecord = playAndRecord
self.ambient = ambient
self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused
self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession
@ -368,7 +370,7 @@ private final class MediaPlayerContext {
self.audioRenderer = nil self.audioRenderer = nil
let queue = self.queue 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 { queue.async {
if let strongSelf = self { if let strongSelf = self {
strongSelf.tick() strongSelf.tick()
@ -446,7 +448,7 @@ private final class MediaPlayerContext {
self.lastStatusUpdateTimestamp = nil self.lastStatusUpdateTimestamp = nil
if self.enableSound { if self.enableSound {
let queue = self.queue 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 { queue.async {
if let strongSelf = self { if let strongSelf = self {
strongSelf.tick() 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 let audioLevelPipe = self.audioLevelPipe
self.queue.async { 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) self.contextRef = Unmanaged.passRetained(context)
} }
} }

View File

@ -239,6 +239,7 @@ private final class AudioPlayerRendererContext {
let audioSessionDisposable = MetaDisposable() let audioSessionDisposable = MetaDisposable()
var audioSessionControl: ManagedAudioSessionControl? var audioSessionControl: ManagedAudioSessionControl?
let playAndRecord: Bool let playAndRecord: Bool
let ambient: Bool
var forceAudioToSpeaker: Bool { var forceAudioToSpeaker: Bool {
didSet { didSet {
if self.forceAudioToSpeaker != oldValue { 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<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, ambient: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
assert(audioPlayerRendererQueue.isCurrent()) assert(audioPlayerRendererQueue.isCurrent())
self.audioSession = audioSession self.audioSession = audioSession
@ -262,6 +263,7 @@ private final class AudioPlayerRendererContext {
self.audioPaused = audioPaused self.audioPaused = audioPaused
self.playAndRecord = playAndRecord self.playAndRecord = playAndRecord
self.ambient = ambient
self.audioStreamDescription = audioRendererNativeStreamDescription() self.audioStreamDescription = audioRendererNativeStreamDescription()
@ -481,7 +483,7 @@ private final class AudioPlayerRendererContext {
switch self.audioSession { switch self.audioSession {
case let .manager(manager): 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 { audioPlayerRendererQueue.async {
if let strongSelf = self { if let strongSelf = self {
strongSelf.audioSessionControl = control strongSelf.audioSessionControl = control
@ -751,7 +753,7 @@ public final class MediaPlayerAudioRenderer {
private let audioClock: CMClock private let audioClock: CMClock
public let audioTimebase: CMTimebase public let audioTimebase: CMTimebase
public init(audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { public init(audioSession: MediaPlayerAudioSessionControl, playAndRecord: Bool, ambient: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
var audioClock: CMClock? var audioClock: CMClock?
CMAudioClockCreate(allocator: nil, clockOut: &audioClock) CMAudioClockCreate(allocator: nil, clockOut: &audioClock)
if audioClock == nil { if audioClock == nil {
@ -764,7 +766,7 @@ public final class MediaPlayerAudioRenderer {
self.audioTimebase = audioTimebase! self.audioTimebase = audioTimebase!
audioPlayerRendererQueue.async { 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) self.contextRef = Unmanaged.passRetained(context)
} }
} }

View File

@ -4,6 +4,7 @@ import AVFoundation
import UIKit import UIKit
public enum ManagedAudioSessionType: Equatable { public enum ManagedAudioSessionType: Equatable {
case ambient
case play case play
case playWithPossiblePortOverride case playWithPossiblePortOverride
case record(speaker: Bool) case record(speaker: Bool)
@ -22,6 +23,8 @@ public enum ManagedAudioSessionType: Equatable {
private func nativeCategoryForType(_ type: ManagedAudioSessionType, headphones: Bool, outputMode: AudioSessionOutputMode) -> AVAudioSession.Category { private func nativeCategoryForType(_ type: ManagedAudioSessionType, headphones: Bool, outputMode: AudioSessionOutputMode) -> AVAudioSession.Category {
switch type { switch type {
case .ambient:
return .ambient
case .play: case .play:
return .playback return .playback
case .record, .voiceCall, .videoCall: case .record, .voiceCall, .videoCall:
@ -665,7 +668,7 @@ public final class ManagedAudioSession {
print("ManagedAudioSession setting category for \(type) (native: \(nativeCategory))") print("ManagedAudioSession setting category for \(type) (native: \(nativeCategory))")
var options: AVAudioSession.CategoryOptions = [] var options: AVAudioSession.CategoryOptions = []
switch type { switch type {
case .play: case .play, .ambient:
break break
case .playWithPossiblePortOverride: case .playWithPossiblePortOverride:
if case .playAndRecord = nativeCategory { if case .playAndRecord = nativeCategory {

View File

@ -34,7 +34,7 @@ private final class PresentationCallToneRenderer {
self.toneRenderer = MediaPlayerAudioRenderer(audioSession: .custom({ control in self.toneRenderer = MediaPlayerAudioRenderer(audioSession: .custom({ control in
return controlImpl?(control) ?? EmptyDisposable 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 controlImpl = { [weak self] control in
queue.async { queue.async {

View File

@ -5232,62 +5232,104 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
} }
}, unpinMessage: { [weak self] id, askForConfirmation in }, unpinMessage: { [weak self] id, askForConfirmation, contextController in
if let strongSelf = self { let impl: () -> Void = {
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { guard let strongSelf = self else {
if strongSelf.canManagePin() { return
let action: () -> Void = { }
if let strongSelf = self { guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
let disposable: MetaDisposable return
if let current = strongSelf.unpinMessageDisposable { }
disposable = current
} else { if strongSelf.canManagePin() {
disposable = MetaDisposable() let action: () -> Void = {
strongSelf.unpinMessageDisposable = disposable if let strongSelf = self {
} let disposable: MetaDisposable
if let current = strongSelf.unpinMessageDisposable {
if askForConfirmation { disposable = current
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = true } else {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { disposable = MetaDisposable()
return $0.updatedPendingUnpinnedAllMessages(true) 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( strongSelf.present(
UndoOverlayController( UndoOverlayController(
presentationData: strongSelf.presentationData, presentationData: strongSelf.presentationData,
content: .messagesUnpinned( content: .messagesUnpinned(
title: strongSelf.presentationData.strings.Chat_MessagesUnpinned(1), title: strongSelf.presentationData.strings.Chat_MessagesUnpinned(1),
text: "", text: "",
undo: askForConfirmation, undo: true,
isHidden: false isHidden: false
), ),
elevatedLayout: false, elevatedLayout: false,
action: { action in action: { action in
guard let strongSelf = self else {
return true
}
switch action { switch action {
case .commit: case .commit:
disposable.set((requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id)) let _ = (requestUpdatePinnedMessage(account: strongSelf.context.account, peerId: peer.id, update: .clear(id: id))
|> deliverOnMainQueue).start(error: { _ in |> deliverOnMainQueue).start(completed: {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.chatDisplayNode.historyNode.pendingUnpinnedAllMessages = false strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.remove(id)
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)
}) })
case .undo:
strongSelf.chatDisplayNode.historyNode.pendingRemovedMessages.remove(id)
default: default:
break 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: { if askForConfirmation {
action() 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: {
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root))
} else {
action() action()
} }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root))
} else { } else {
if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage { action()
let previousClosedPinnedMessageId = strongSelf.presentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId }
} else {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { if let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage {
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in let previousClosedPinnedMessageId = strongSelf.presentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId
var value = value
value.closedPinnedMessageId = pinnedMessage.topMessageId strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return value return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
}) }) var value = value
}) value.closedPinnedMessageId = pinnedMessage.topMessageId
strongSelf.present( return value
UndoOverlayController( }) })
presentationData: strongSelf.presentationData, })
content: .messagesUnpinned( strongSelf.present(
title: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenTitle, UndoOverlayController(
text: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenText, presentationData: strongSelf.presentationData,
undo: true, content: .messagesUnpinned(
isHidden: false title: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenTitle,
), text: strongSelf.presentationData.strings.Chat_PinnedMessagesHiddenText,
elevatedLayout: false, undo: true,
action: { action in isHidden: false
guard let strongSelf = self else { ),
return true elevatedLayout: false,
} action: { action in
switch action { guard let strongSelf = self else {
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 return true
} }
), switch action {
in: .current case .commit:
) break
strongSelf.updatedClosedPinnedMessageId?(pinnedMessage.topMessageId) 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 }, unpinAllMessages: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return

View File

@ -8,7 +8,7 @@ import AccountContext
import TelegramPresentationData import TelegramPresentationData
func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, includeSearchEntry: Bool, reverse: Bool, groupMessages: Bool, selectedMessages: Set<MessageId>?, 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<MessageId>?, presentationData: ChatPresentationData, historyAppearsCleared: Bool, pendingUnpinnedAllMessages: Bool, pendingRemovedMessages: Set<MessageId>, associatedData: ChatMessageItemAssociatedData, updatingMedia: [MessageId: ChatUpdatingMessageMedia], customChannelDiscussionReadState: MessageId?, customThreadOutgoingReadState: MessageId?) -> [ChatHistoryEntry] {
if historyAppearsCleared { if historyAppearsCleared {
return [] return []
} }
@ -34,6 +34,10 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
var message = entry.message var message = entry.message
var isRead = entry.isRead var isRead = entry.isRead
if pendingRemovedMessages.contains(message.id) {
continue
}
if let customThreadOutgoingReadState = customThreadOutgoingReadState { if let customThreadOutgoingReadState = customThreadOutgoingReadState {
isRead = customThreadOutgoingReadState >= message.id isRead = customThreadOutgoingReadState >= message.id
} }

View File

@ -565,6 +565,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
} }
private let pendingRemovedMessagesPromise = ValuePromise<Set<MessageId>>(Set())
var pendingRemovedMessages: Set<MessageId> = Set() {
didSet {
if self.pendingRemovedMessages != oldValue {
self.pendingRemovedMessagesPromise.set(self.pendingRemovedMessages)
}
}
}
private(set) var isScrollAtBottomPosition = false private(set) var isScrollAtBottomPosition = false
public var isScrollAtBottomPositionUpdated: (() -> Void)? public var isScrollAtBottomPositionUpdated: (() -> Void)?
@ -846,10 +855,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
automaticDownloadNetworkType, automaticDownloadNetworkType,
self.historyAppearsClearedPromise.get(), self.historyAppearsClearedPromise.get(),
self.pendingUnpinnedAllMessagesPromise.get(), self.pendingUnpinnedAllMessagesPromise.get(),
self.pendingRemovedMessagesPromise.get(),
animatedEmojiStickers, animatedEmojiStickers,
customChannelDiscussionReadState, customChannelDiscussionReadState,
customThreadOutgoingReadState 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() { func applyHole() {
Queue.mainQueue().async { Queue.mainQueue().async {
if let strongSelf = self { 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 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 lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0
let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id) let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id)
let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages)) let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages))

View File

@ -687,9 +687,8 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
if let pinnedSelectedMessageId = pinnedSelectedMessageId { if let pinnedSelectedMessageId = pinnedSelectedMessageId {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Unpin, icon: { theme in 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unpin"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in }, action: { c, _ in
interfaceInteraction.unpinMessage(pinnedSelectedMessageId, false) interfaceInteraction.unpinMessage(pinnedSelectedMessageId, false, c)
f(.default)
}))) })))
} else { } else {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Pin, icon: { theme in actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Pin, icon: { theme in

View File

@ -176,6 +176,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var highlightedState: Bool = false private var highlightedState: Bool = false
private var haptic: EmojiHaptic? private var haptic: EmojiHaptic?
private var mediaPlayer: MediaPlayer?
private let mediaStatusDisposable = MetaDisposable()
private var currentSwipeToReplyTranslation: CGFloat = 0.0 private var currentSwipeToReplyTranslation: CGFloat = 0.0
@ -246,6 +248,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
deinit { deinit {
self.disposable.dispose() self.disposable.dispose()
self.mediaStatusDisposable.set(nil)
} }
required init?(coder aDecoder: NSCoder) { 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 beatingHearts: [UInt32] = [0x2764, 0x1F90E, 0x1F9E1, 0x1F499, 0x1F49A, 0x1F49C, 0x1F49B, 0x1F5A4, 0x1F90D]
let peach = 0x1F351 let peach = 0x1F351
let coffin = 0x26B0
let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> take(1) |> take(1)
@ -1271,16 +1275,46 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
if shouldPlay { if shouldPlay {
let _ = (appConfiguration let _ = (appConfiguration
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration in |> deliverOnMainQueue).start(next: { [weak self] appConfiguration in
guard let strongSelf = self else {
return
}
let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: item.context.account) let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: item.context.account)
for (emoji, file) in emojiSounds.sounds { for (emoji, file) in emojiSounds.sounds {
if emoji.unicodeScalars.first == firstScalar { if emoji.unicodeScalars.first == firstScalar {
let mediaManager = item.context.sharedContext.mediaManager 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() mediaPlayer.togglePlayPause()
self?.mediaPlayer = mediaPlayer mediaPlayer.actionAtEnd = .action({ [weak self] in
self?.mediaPlayer = nil
animationNode.play() })
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 break
} }
} }
@ -1303,8 +1337,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return nil return nil
} }
private var mediaPlayer: MediaPlayer?
@objc private func shareButtonPressed() { @objc private func shareButtonPressed() {
if let item = self.item { if let item = self.item {
if case .pinnedMessages = item.associatedData.subject { if case .pinnedMessages = item.associatedData.subject {

View File

@ -544,33 +544,34 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let subnodes = self.subnodes { func process(node: ASDisplayNode) {
for node in subnodes { if node === self.accessoryItemNode {
if let contextNode = node as? ContextExtractedContentContainingNode { return
if let contextSubnodes = contextNode.contentNode.subnodes { }
inner: for contextSubnode in contextSubnodes {
if contextSubnode !== self.accessoryItemNode { if node !== self {
if contextSubnode == self.backgroundNode { switch node {
if self.backgroundNode.hasImage && self.backgroundWallpaperNode.hasImage { case _ as ContextExtractedContentContainingNode, _ as ContextControllerSourceNode, _ as ContextExtractedContentNode:
continue inner break
} default:
}
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 {
node.layer.allowsGroupOpacity = true 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.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak node] _ in
node?.layer.allowsGroupOpacity = false 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) { override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
@ -588,10 +589,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
override func animateAdded(_ currentTimestamp: Double, duration: Double) { override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration) super.animateAdded(currentTimestamp, duration: duration)
self.allowsGroupOpacity = true if let subnodes = self.subnodes {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in for subnode in subnodes {
self?.allowsGroupOpacity = false 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() { override func didLoad() {

View File

@ -95,7 +95,7 @@ final class ChatPanelInterfaceInteraction {
let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Bool
let unblockPeer: () -> Void let unblockPeer: () -> Void
let pinMessage: (MessageId, ContextController?) -> Void let pinMessage: (MessageId, ContextController?) -> Void
let unpinMessage: (MessageId, Bool) -> Void let unpinMessage: (MessageId, Bool, ContextController?) -> Void
let unpinAllMessages: () -> Void let unpinAllMessages: () -> Void
let openPinnedList: (MessageId) -> Void let openPinnedList: (MessageId) -> Void
let shareAccountContact: () -> Void let shareAccountContact: () -> Void
@ -174,7 +174,7 @@ final class ChatPanelInterfaceInteraction {
sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool,
unblockPeer: @escaping () -> Void, unblockPeer: @escaping () -> Void,
pinMessage: @escaping (MessageId, ContextController?) -> Void, pinMessage: @escaping (MessageId, ContextController?) -> Void,
unpinMessage: @escaping (MessageId, Bool) -> Void, unpinMessage: @escaping (MessageId, Bool, ContextController?) -> Void,
unpinAllMessages: @escaping () -> Void, unpinAllMessages: @escaping () -> Void,
openPinnedList: @escaping (MessageId) -> Void, openPinnedList: @escaping (MessageId) -> Void,
shareAccountContact: @escaping () -> Void, shareAccountContact: @escaping () -> Void,

View File

@ -462,7 +462,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
@objc func closePressed() { @objc func closePressed() {
if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage { if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage {
interfaceInteraction.unpinMessage(message.message.id, true) interfaceInteraction.unpinMessage(message.message.id, true, nil)
} }
} }

View File

@ -100,7 +100,7 @@ final class ChatRecentActionsController: TelegramBaseController {
return false return false
}, unblockPeer: { }, unblockPeer: {
}, pinMessage: { _, _ in }, pinMessage: { _, _ in
}, unpinMessage: { _, _ in }, unpinMessage: { _, _, _ in
}, unpinAllMessages: { }, unpinAllMessages: {
}, openPinnedList: { _ in }, openPinnedList: { _ in
}, shareAccountContact: { }, shareAccountContact: {

View File

@ -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()
}
}

View File

@ -214,7 +214,7 @@ final class ManagedAudioRecorderContext {
} }
return ActionDisposable { return ActionDisposable {
} }
}), playAndRecord: true, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe<Float>(), updatedRate: { }), playAndRecord: true, ambient: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe<Float>(), updatedRate: {
}, audioPaused: {}) }, audioPaused: {})
self.toneRenderer = toneRenderer self.toneRenderer = toneRenderer

View File

@ -406,7 +406,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
return false return false
}, unblockPeer: { }, unblockPeer: {
}, pinMessage: { _, _ in }, pinMessage: { _, _ in
}, unpinMessage: { _, _ in }, unpinMessage: { _, _, _ in
}, unpinAllMessages: { }, unpinAllMessages: {
}, openPinnedList: { _ in }, openPinnedList: { _ in
}, shareAccountContact: { }, shareAccountContact: {