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.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)
}
}
}

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

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

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
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))

View File

@ -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

View File

@ -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 {

View File

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

View File

@ -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,

View File

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

View File

@ -100,7 +100,7 @@ final class ChatRecentActionsController: TelegramBaseController {
return false
}, unblockPeer: {
}, pinMessage: { _, _ in
}, unpinMessage: { _, _ in
}, unpinMessage: { _, _, _ in
}, unpinAllMessages: {
}, openPinnedList: { _ in
}, 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 {
}
}), 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

View File

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