mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
36784fab3d
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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<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())
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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<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())
|
||||
|
||||
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<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?
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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<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 {
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
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))
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,7 @@ final class ChatRecentActionsController: TelegramBaseController {
|
||||
return false
|
||||
}, unblockPeer: {
|
||||
}, pinMessage: { _, _ in
|
||||
}, unpinMessage: { _, _ in
|
||||
}, unpinMessage: { _, _, _ in
|
||||
}, unpinAllMessages: {
|
||||
}, openPinnedList: { _ in
|
||||
}, shareAccountContact: {
|
||||
|
74
submodules/TelegramUI/Sources/CoffinHaptic.swift
Normal file
74
submodules/TelegramUI/Sources/CoffinHaptic.swift
Normal 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()
|
||||
}
|
||||
}
|
@ -214,7 +214,7 @@ final class ManagedAudioRecorderContext {
|
||||
}
|
||||
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: {})
|
||||
self.toneRenderer = toneRenderer
|
||||
|
||||
|
@ -406,7 +406,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
|
||||
return false
|
||||
}, unblockPeer: {
|
||||
}, pinMessage: { _, _ in
|
||||
}, unpinMessage: { _, _ in
|
||||
}, unpinMessage: { _, _, _ in
|
||||
}, unpinAllMessages: {
|
||||
}, openPinnedList: { _ in
|
||||
}, shareAccountContact: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user