mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' into experiments/parallel-swiftmodule
This commit is contained in:
commit
2172bf5213
@ -9,9 +9,9 @@ public enum DeviceLocationMode: Int32 {
|
|||||||
private final class DeviceLocationSubscriber {
|
private final class DeviceLocationSubscriber {
|
||||||
let id: Int32
|
let id: Int32
|
||||||
let mode: DeviceLocationMode
|
let mode: DeviceLocationMode
|
||||||
let update: (CLLocationCoordinate2D, Double, Double?) -> Void
|
let update: (CLLocation, Double?) -> Void
|
||||||
|
|
||||||
init(id: Int32, mode: DeviceLocationMode, update: @escaping (CLLocationCoordinate2D, Double, Double?) -> Void) {
|
init(id: Int32, mode: DeviceLocationMode, update: @escaping (CLLocation, Double?) -> Void) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.update = update
|
self.update = update
|
||||||
@ -39,7 +39,7 @@ public final class DeviceLocationManager: NSObject {
|
|||||||
private var subscribers: [DeviceLocationSubscriber] = []
|
private var subscribers: [DeviceLocationSubscriber] = []
|
||||||
private var currentTopMode: DeviceLocationMode?
|
private var currentTopMode: DeviceLocationMode?
|
||||||
|
|
||||||
private var currentLocation: (CLLocationCoordinate2D, Double)?
|
private var currentLocation: CLLocation?
|
||||||
private var currentHeading: CLHeading?
|
private var currentHeading: CLHeading?
|
||||||
|
|
||||||
public init(queue: Queue, log: ((String) -> Void)? = nil) {
|
public init(queue: Queue, log: ((String) -> Void)? = nil) {
|
||||||
@ -56,12 +56,12 @@ public final class DeviceLocationManager: NSObject {
|
|||||||
}
|
}
|
||||||
self.manager.delegate = self
|
self.manager.delegate = self
|
||||||
self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
|
self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
|
||||||
self.manager.distanceFilter = 10.0
|
self.manager.distanceFilter = 5.0
|
||||||
self.manager.activityType = .other
|
self.manager.activityType = .other
|
||||||
self.manager.pausesLocationUpdatesAutomatically = false
|
self.manager.pausesLocationUpdatesAutomatically = false
|
||||||
}
|
}
|
||||||
|
|
||||||
public func push(mode: DeviceLocationMode, updated: @escaping (CLLocationCoordinate2D, Double, Double?) -> Void) -> Disposable {
|
public func push(mode: DeviceLocationMode, updated: @escaping (CLLocation, Double?) -> Void) -> Disposable {
|
||||||
assert(self.queue.isCurrent())
|
assert(self.queue.isCurrent())
|
||||||
|
|
||||||
let id = self.nextSubscriberId
|
let id = self.nextSubscriberId
|
||||||
@ -69,7 +69,7 @@ public final class DeviceLocationManager: NSObject {
|
|||||||
self.subscribers.append(DeviceLocationSubscriber(id: id, mode: mode, update: updated))
|
self.subscribers.append(DeviceLocationSubscriber(id: id, mode: mode, update: updated))
|
||||||
|
|
||||||
if let currentLocation = self.currentLocation {
|
if let currentLocation = self.currentLocation {
|
||||||
updated(currentLocation.0, currentLocation.1, self.currentHeading?.magneticHeading)
|
updated(currentLocation, self.currentHeading?.magneticHeading)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateTopMode()
|
self.updateTopMode()
|
||||||
@ -119,15 +119,28 @@ public final class DeviceLocationManager: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension CLHeading {
|
||||||
|
var effectiveHeading: Double? {
|
||||||
|
if self.headingAccuracy < 0.0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if self.trueHeading > 0.0 {
|
||||||
|
return self.trueHeading
|
||||||
|
} else {
|
||||||
|
return self.magneticHeading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension DeviceLocationManager: CLLocationManagerDelegate {
|
extension DeviceLocationManager: CLLocationManagerDelegate {
|
||||||
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
assert(self.queue.isCurrent())
|
assert(self.queue.isCurrent())
|
||||||
|
|
||||||
if let location = locations.first {
|
if let location = locations.first {
|
||||||
if self.currentTopMode != nil {
|
if self.currentTopMode != nil {
|
||||||
self.currentLocation = (location.coordinate, location.horizontalAccuracy)
|
self.currentLocation = location
|
||||||
for subscriber in self.subscribers {
|
for subscriber in self.subscribers {
|
||||||
subscriber.update(location.coordinate, location.horizontalAccuracy, self.currentHeading?.magneticHeading)
|
subscriber.update(location, self.currentHeading?.effectiveHeading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +153,7 @@ extension DeviceLocationManager: CLLocationManagerDelegate {
|
|||||||
self.currentHeading = newHeading
|
self.currentHeading = newHeading
|
||||||
if let currentLocation = self.currentLocation {
|
if let currentLocation = self.currentLocation {
|
||||||
for subscriber in self.subscribers {
|
for subscriber in self.subscribers {
|
||||||
subscriber.update(currentLocation.0, currentLocation.1, newHeading.magneticHeading)
|
subscriber.update(currentLocation, newHeading.effectiveHeading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,8 +163,8 @@ extension DeviceLocationManager: CLLocationManagerDelegate {
|
|||||||
public func currentLocationManagerCoordinate(manager: DeviceLocationManager, timeout timeoutValue: Double) -> Signal<CLLocationCoordinate2D?, NoError> {
|
public func currentLocationManagerCoordinate(manager: DeviceLocationManager, timeout timeoutValue: Double) -> Signal<CLLocationCoordinate2D?, NoError> {
|
||||||
return (
|
return (
|
||||||
Signal { subscriber in
|
Signal { subscriber in
|
||||||
let disposable = manager.push(mode: .precise, updated: { coordinate, _, _ in
|
let disposable = manager.push(mode: .precise, updated: { location, _ in
|
||||||
subscriber.putNext(coordinate)
|
subscriber.putNext(location.coordinate)
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
})
|
})
|
||||||
return disposable
|
return disposable
|
||||||
|
@ -355,6 +355,8 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let initialPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden()
|
||||||
|
|
||||||
var overlayLayout = layout
|
var overlayLayout = layout
|
||||||
|
|
||||||
if let globalOverlayContainerParent = self.globalOverlayContainerParent {
|
if let globalOverlayContainerParent = self.globalOverlayContainerParent {
|
||||||
@ -988,10 +990,15 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
self.isUpdatingContainers = false
|
self.isUpdatingContainers = false
|
||||||
|
|
||||||
if notifyGlobalOverlayControllersUpdated {
|
if notifyGlobalOverlayControllersUpdated {
|
||||||
self.globalOverlayControllersUpdated?()
|
self.internalGlobalOverlayControllersUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateSupportedOrientations?()
|
self.updateSupportedOrientations?()
|
||||||
|
|
||||||
|
let updatedPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden()
|
||||||
|
if initialPrefersOnScreenNavigationHidden != updatedPrefersOnScreenNavigationHidden {
|
||||||
|
self.currentWindow?.invalidatePrefersOnScreenNavigationHidden()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func controllerRemoved(_ controller: ViewController) {
|
private func controllerRemoved(_ controller: ViewController) {
|
||||||
@ -1184,7 +1191,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
if overlayContainer.controller === controller {
|
if overlayContainer.controller === controller {
|
||||||
overlayContainer.removeFromSupernode()
|
overlayContainer.removeFromSupernode()
|
||||||
strongSelf.globalOverlayContainers.remove(at: i)
|
strongSelf.globalOverlayContainers.remove(at: i)
|
||||||
strongSelf.globalOverlayControllersUpdated?()
|
strongSelf.internalGlobalOverlayControllersUpdated()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1194,6 +1201,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
if overlayContainer.controller === controller {
|
if overlayContainer.controller === controller {
|
||||||
overlayContainer.removeFromSupernode()
|
overlayContainer.removeFromSupernode()
|
||||||
strongSelf.overlayContainers.remove(at: i)
|
strongSelf.overlayContainers.remove(at: i)
|
||||||
|
strongSelf.internalOverlayControllersUpdated()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1395,4 +1403,21 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func internalGlobalOverlayControllersUpdated() {
|
||||||
|
self.globalOverlayControllersUpdated?()
|
||||||
|
self.currentWindow?.invalidatePrefersOnScreenNavigationHidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func internalOverlayControllersUpdated() {
|
||||||
|
self.currentWindow?.invalidatePrefersOnScreenNavigationHidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func collectPrefersOnScreenNavigationHidden() -> Bool {
|
||||||
|
var hidden = false
|
||||||
|
if let overlayController = self.topOverlayController {
|
||||||
|
hidden = hidden || overlayController.prefersOnScreenNavigationHidden
|
||||||
|
}
|
||||||
|
return hidden
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,9 @@ public class TwoAxisStepBarsChartController: BaseLinesChartController {
|
|||||||
|
|
||||||
private var prevoiusHorizontalStrideInterval: Int = 1
|
private var prevoiusHorizontalStrideInterval: Int = 1
|
||||||
|
|
||||||
|
public var hourly: Bool = false
|
||||||
|
public var min5: Bool = false
|
||||||
|
|
||||||
override public init(chartsCollection: ChartsCollection) {
|
override public init(chartsCollection: ChartsCollection) {
|
||||||
self.initialChartCollection = chartsCollection
|
self.initialChartCollection = chartsCollection
|
||||||
graphControllers = chartsCollection.chartValues.map { _ in GraphController() }
|
graphControllers = chartsCollection.chartValues.map { _ in GraphController() }
|
||||||
@ -252,8 +255,19 @@ public class TwoAxisStepBarsChartController: BaseLinesChartController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||||
|
var scaleType: ChartScaleType = .day
|
||||||
|
if isZoomed {
|
||||||
|
scaleType = .minutes5
|
||||||
|
} else {
|
||||||
|
if self.hourly {
|
||||||
|
scaleType = .hour
|
||||||
|
} else if self.min5 {
|
||||||
|
scaleType = .minutes5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
|
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
|
||||||
scaleType: isZoomed ? .minutes5 : .day,
|
scaleType: scaleType,
|
||||||
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
|
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
|
||||||
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||||
self.prevoiusHorizontalStrideInterval = stride
|
self.prevoiusHorizontalStrideInterval = stride
|
||||||
|
@ -16,6 +16,8 @@ public enum ChartType {
|
|||||||
case step
|
case step
|
||||||
case twoAxisStep
|
case twoAxisStep
|
||||||
case hourlyStep
|
case hourlyStep
|
||||||
|
case twoAxisHourlyStep
|
||||||
|
case twoAxis5MinStep
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ChartTheme {
|
public extension ChartTheme {
|
||||||
@ -90,6 +92,16 @@ public func createChartController(_ data: String, type: ChartType, getDetailsDat
|
|||||||
case .hourlyStep:
|
case .hourlyStep:
|
||||||
controller = StepBarsChartController(chartsCollection: collection, hourly: true)
|
controller = StepBarsChartController(chartsCollection: collection, hourly: true)
|
||||||
controller.isZoomable = false
|
controller.isZoomable = false
|
||||||
|
case .twoAxisHourlyStep:
|
||||||
|
let stepController = TwoAxisStepBarsChartController(chartsCollection: collection)
|
||||||
|
stepController.hourly = true
|
||||||
|
controller = stepController
|
||||||
|
controller.isZoomable = false
|
||||||
|
case .twoAxis5MinStep:
|
||||||
|
let stepController = TwoAxisStepBarsChartController(chartsCollection: collection)
|
||||||
|
stepController.min5 = true
|
||||||
|
controller = stepController
|
||||||
|
controller.isZoomable = false
|
||||||
}
|
}
|
||||||
controller.getDetailsData = { date, completion in
|
controller.getDetailsData = { date, completion in
|
||||||
getDetailsData(date, { detailsData in
|
getDetailsData(date, { detailsData in
|
||||||
|
@ -107,9 +107,13 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
|
|||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
if value {
|
if value {
|
||||||
let queue = strongSelf.queue
|
let queue = strongSelf.queue
|
||||||
strongSelf.deviceLocationDisposable.set(strongSelf.locationManager.push(mode: .precise, updated: { coordinate, accuracyRadius, heading in
|
strongSelf.deviceLocationDisposable.set(strongSelf.locationManager.push(mode: .precise, updated: { location, heading in
|
||||||
queue.async {
|
queue.async {
|
||||||
self?.updateDeviceCoordinate(coordinate, accuracyRadius: accuracyRadius, heading: heading)
|
var effectiveHeading = heading ?? location.course
|
||||||
|
if location.speed > 1.0 {
|
||||||
|
effectiveHeading = location.course
|
||||||
|
}
|
||||||
|
self?.updateDeviceCoordinate(location.coordinate, accuracyRadius: location.horizontalAccuracy, heading: effectiveHeading)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
@ -213,7 +217,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
|
|||||||
let ids = self.broadcastToMessageIds
|
let ids = self.broadcastToMessageIds
|
||||||
let remainingIds = Atomic<Set<MessageId>>(value: Set(ids.keys))
|
let remainingIds = Atomic<Set<MessageId>>(value: Set(ids.keys))
|
||||||
for id in ids.keys {
|
for id in ids.keys {
|
||||||
self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, stop: false, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: Int32(accuracyRadius)), heading: Int32(heading ?? 0), proximityNotificationRadius: nil)
|
self.editMessageDisposables.set((requestEditLiveLocation(postbox: self.postbox, network: self.network, stateManager: self.stateManager, messageId: id, stop: false, coordinate: (latitude: coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: Int32(accuracyRadius)), heading: heading.flatMap { Int32($0) }, proximityNotificationRadius: nil)
|
||||||
|> deliverOn(self.queue)).start(completed: { [weak self] in
|
|> deliverOn(self.queue)).start(completed: { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.editMessageDisposables.set(nil, forKey: id)
|
strongSelf.editMessageDisposables.set(nil, forKey: id)
|
||||||
|
@ -202,7 +202,6 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
private var state: LocationViewState
|
private var state: LocationViewState
|
||||||
private let statePromise: Promise<LocationViewState>
|
private let statePromise: Promise<LocationViewState>
|
||||||
private var geocodingDisposable = MetaDisposable()
|
|
||||||
|
|
||||||
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
||||||
private var listOffset: CGFloat?
|
private var listOffset: CGFloat?
|
||||||
@ -271,7 +270,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
setupProximityNotificationImpl = { reset in
|
setupProximityNotificationImpl = { [weak self] reset in
|
||||||
let _ = (liveLocations
|
let _ = (liveLocations
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
||||||
@ -364,7 +363,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
timeout = nil
|
timeout = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let channel = subject.author as? TelegramChannel, case .broadcast = channel.info {
|
if let channel = subject.author as? TelegramChannel, case .broadcast = channel.info, activeOwnLiveLocation == nil {
|
||||||
} else {
|
} else {
|
||||||
entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, userLocation?.coordinate, beginTime, timeout))
|
entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, userLocation?.coordinate, beginTime, timeout))
|
||||||
}
|
}
|
||||||
@ -388,6 +387,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
}
|
}
|
||||||
|
|
||||||
for message in effectiveLiveLocations {
|
for message in effectiveLiveLocations {
|
||||||
|
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, message.threadId != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var liveBroadcastingTimeout: Int32 = 0
|
var liveBroadcastingTimeout: Int32 = 0
|
||||||
if let location = getLocation(from: message), let timeout = location.liveBroadcastingTimeout {
|
if let location = getLocation(from: message), let timeout = location.liveBroadcastingTimeout {
|
||||||
liveBroadcastingTimeout = timeout
|
liveBroadcastingTimeout = timeout
|
||||||
@ -536,13 +539,19 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.disposable?.dispose()
|
self.disposable?.dispose()
|
||||||
self.geocodingDisposable.dispose()
|
|
||||||
|
|
||||||
self.locationManager.manager.stopUpdatingHeading()
|
self.locationManager.manager.stopUpdatingHeading()
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
|
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
|
||||||
self.headerNode.mapNode.userHeading = CGFloat(newHeading.magneticHeading)
|
if newHeading.headingAccuracy < 0.0 {
|
||||||
|
self.headerNode.mapNode.userHeading = nil
|
||||||
|
}
|
||||||
|
if newHeading.trueHeading > 0.0 {
|
||||||
|
self.headerNode.mapNode.userHeading = CGFloat(newHeading.trueHeading)
|
||||||
|
} else {
|
||||||
|
self.headerNode.mapNode.userHeading = CGFloat(newHeading.magneticHeading)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePresentationData(_ presentationData: PresentationData) {
|
func updatePresentationData(_ presentationData: PresentationData) {
|
||||||
|
@ -385,8 +385,8 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon
|
|||||||
|
|
||||||
let forceUpdateLocation: () -> Void = {
|
let forceUpdateLocation: () -> Void = {
|
||||||
let locationCoordinates = Signal<(Double, Double), NoError> { subscriber in
|
let locationCoordinates = Signal<(Double, Double), NoError> { subscriber in
|
||||||
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.precise, updated: { coordinate, _, _ in
|
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.precise, updated: { location, _ in
|
||||||
subscriber.putNext((coordinate.latitude, coordinate.longitude))
|
subscriber.putNext((location.coordinate.latitude, location.coordinate.longitude))
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -135,16 +135,15 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||||||
})
|
})
|
||||||
}, sectionId: self.section, style: .blocks)
|
}, sectionId: self.section, style: .blocks)
|
||||||
case let .publicForward(_, _, _, _, message):
|
case let .publicForward(_, _, _, _, message):
|
||||||
var views: Int = 0
|
var views: Int32 = 0
|
||||||
for attribute in message.attributes {
|
for attribute in message.attributes {
|
||||||
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
|
||||||
views = viewsAttribute.count
|
views = Int32(viewsAttribute.count)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var text: String = ""
|
let text: String = presentationData.strings.Stats_MessageViews(views)
|
||||||
text += "\(views) views"
|
|
||||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ",", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: message.peers[message.id.peerId]!, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
|
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ",", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: message.peers[message.id.peerId]!, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
|
||||||
arguments.openMessage(message.id)
|
arguments.openMessage(message.id)
|
||||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil)
|
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil)
|
||||||
@ -161,7 +160,17 @@ private func messageStatsControllerEntries(data: MessageStats?, messages: Search
|
|||||||
|
|
||||||
if !data.interactionsGraph.isEmpty {
|
if !data.interactionsGraph.isEmpty {
|
||||||
entries.append(.interactionsTitle(presentationData.theme, presentationData.strings.Stats_MessageInteractionsTitle.uppercased()))
|
entries.append(.interactionsTitle(presentationData.theme, presentationData.strings.Stats_MessageInteractionsTitle.uppercased()))
|
||||||
entries.append(.interactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
|
|
||||||
|
var chartType: ChartType
|
||||||
|
if data.interactionsGraphDelta == 3600 {
|
||||||
|
chartType = .twoAxisHourlyStep
|
||||||
|
} else if data.interactionsGraphDelta == 300 {
|
||||||
|
chartType = .twoAxis5MinStep
|
||||||
|
} else {
|
||||||
|
chartType = .twoAxisStep
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(.interactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, chartType))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let messages = messages, !messages.messages.isEmpty {
|
if let messages = messages, !messages.messages.isEmpty {
|
||||||
|
@ -9,13 +9,13 @@ public struct MessageStats: Equatable {
|
|||||||
public let views: Int
|
public let views: Int
|
||||||
public let forwards: Int
|
public let forwards: Int
|
||||||
public let interactionsGraph: StatsGraph
|
public let interactionsGraph: StatsGraph
|
||||||
public let detailedInteractionsGraph: StatsGraph?
|
public let interactionsGraphDelta: Int64
|
||||||
|
|
||||||
init(views: Int, forwards: Int, interactionsGraph: StatsGraph, detailedInteractionsGraph: StatsGraph?) {
|
init(views: Int, forwards: Int, interactionsGraph: StatsGraph, interactionsGraphDelta: Int64) {
|
||||||
self.views = views
|
self.views = views
|
||||||
self.forwards = forwards
|
self.forwards = forwards
|
||||||
self.interactionsGraph = interactionsGraph
|
self.interactionsGraph = interactionsGraph
|
||||||
self.detailedInteractionsGraph = detailedInteractionsGraph
|
self.interactionsGraphDelta = interactionsGraphDelta
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool {
|
public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool {
|
||||||
@ -28,14 +28,14 @@ public struct MessageStats: Equatable {
|
|||||||
if lhs.interactionsGraph != rhs.interactionsGraph {
|
if lhs.interactionsGraph != rhs.interactionsGraph {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.detailedInteractionsGraph != rhs.detailedInteractionsGraph {
|
if lhs.interactionsGraphDelta != rhs.interactionsGraphDelta {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats {
|
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats {
|
||||||
return MessageStats(views: self.views, forwards: self.forwards, interactionsGraph: interactionsGraph, detailedInteractionsGraph: self.detailedInteractionsGraph)
|
return MessageStats(views: self.views, forwards: self.forwards, interactionsGraph: interactionsGraph, interactionsGraphDelta: self.interactionsGraphDelta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,16 +86,24 @@ private func requestMessageStats(postbox: Postbox, network: Network, datacenterI
|
|||||||
|> mapToSignal { result -> Signal<MessageStats?, MTRpcError> in
|
|> mapToSignal { result -> Signal<MessageStats?, MTRpcError> in
|
||||||
if case let .messageStats(apiViewsGraph) = result {
|
if case let .messageStats(apiViewsGraph) = result {
|
||||||
let interactionsGraph = StatsGraph(apiStatsGraph: apiViewsGraph)
|
let interactionsGraph = StatsGraph(apiStatsGraph: apiViewsGraph)
|
||||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
var interactionsGraphDelta: Int64 = 86400
|
||||||
if case let .Loaded(tokenValue, _) = interactionsGraph, let token = tokenValue, Int64(message.timestamp + 60 * 60 * 24 * 2) > Int64(timestamp) {
|
if case let .Loaded(_, data) = interactionsGraph {
|
||||||
return requestGraph(network: network, datacenterId: datacenterId, token: token, x: 1601596800000)
|
if let start = data.range(of: "[\"x\",") {
|
||||||
|> castError(MTRpcError.self)
|
let substring = data.suffix(from: start.upperBound)
|
||||||
|> map { detailedGraph -> MessageStats? in
|
if let end = substring.range(of: "],") {
|
||||||
return MessageStats(views: views, forwards: forwards, interactionsGraph: interactionsGraph, detailedInteractionsGraph: detailedGraph)
|
let valuesString = substring.prefix(through: substring.index(before: end.lowerBound))
|
||||||
|
let values = valuesString.components(separatedBy: ",").compactMap { Int64($0) }
|
||||||
|
if values.count > 1 {
|
||||||
|
let first = values[0]
|
||||||
|
let second = values[1]
|
||||||
|
let delta = abs(second - first) / 1000
|
||||||
|
interactionsGraphDelta = delta
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return .single(MessageStats(views: views, forwards: forwards, interactionsGraph: interactionsGraph, detailedInteractionsGraph: nil))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return .single(MessageStats(views: views, forwards: forwards, interactionsGraph: interactionsGraph, interactionsGraphDelta: interactionsGraphDelta))
|
||||||
} else {
|
} else {
|
||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
|
@ -445,9 +445,9 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||||||
case let .geoProximityReached(fromId, toId, distance):
|
case let .geoProximityReached(fromId, toId, distance):
|
||||||
let distanceString = stringForDistance(strings: strings, distance: Double(distance))
|
let distanceString = stringForDistance(strings: strings, distance: Double(distance))
|
||||||
if toId == accountPeerId {
|
if toId == accountPeerId {
|
||||||
attributedString = addAttributesToStringWithRanges(strings.Notification_ProximityReachedYou(message.peers[fromId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "", distanceString), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]))
|
attributedString = addAttributesToStringWithRanges(strings.Notification_ProximityReachedYou(message.peers[fromId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "", distanceString), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, fromId)]))
|
||||||
} else {
|
} else {
|
||||||
attributedString = addAttributesToStringWithRanges(strings.Notification_ProximityReached(message.peers[fromId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "", distanceString, message.peers[toId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id), (2, toId)]))
|
attributedString = addAttributesToStringWithRanges(strings.Notification_ProximityReached(message.peers[fromId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "", distanceString, message.peers[toId]?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, fromId), (2, toId)]))
|
||||||
}
|
}
|
||||||
case .unknown:
|
case .unknown:
|
||||||
attributedString = nil
|
attributedString = nil
|
||||||
|
@ -20,6 +20,7 @@ import Emoji
|
|||||||
import Markdown
|
import Markdown
|
||||||
import ManagedAnimationNode
|
import ManagedAnimationNode
|
||||||
import SlotMachineAnimationNode
|
import SlotMachineAnimationNode
|
||||||
|
import UniversalMediaPlayer
|
||||||
|
|
||||||
private let nameFont = Font.medium(14.0)
|
private let nameFont = Font.medium(14.0)
|
||||||
private let inlineBotPrefixFont = Font.regular(14.0)
|
private let inlineBotPrefixFont = Font.regular(14.0)
|
||||||
@ -175,6 +176,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
private var highlightedState: Bool = false
|
private var highlightedState: Bool = false
|
||||||
|
|
||||||
private var haptic: EmojiHaptic?
|
private var haptic: EmojiHaptic?
|
||||||
|
private var mediaPlayer: MediaPlayer?
|
||||||
|
private let mediaStatusDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var currentSwipeToReplyTranslation: CGFloat = 0.0
|
private var currentSwipeToReplyTranslation: CGFloat = 0.0
|
||||||
|
|
||||||
@ -245,6 +248,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.disposable.dispose()
|
self.disposable.dispose()
|
||||||
|
self.mediaStatusDisposable.set(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
@ -1217,7 +1221,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
} else if let _ = self.emojiFile {
|
} else if let _ = self.emojiFile {
|
||||||
if let animationNode = self.animationNode as? AnimatedStickerNode {
|
if let animationNode = self.animationNode as? AnimatedStickerNode {
|
||||||
var startTime: Signal<Double, NoError>
|
var startTime: Signal<Double, NoError>
|
||||||
if animationNode.playIfNeeded() {
|
var shouldPlay = false
|
||||||
|
if !animationNode.isPlaying {
|
||||||
|
shouldPlay = true
|
||||||
startTime = .single(0.0)
|
startTime = .single(0.0)
|
||||||
} else {
|
} else {
|
||||||
startTime = animationNode.status
|
startTime = animationNode.status
|
||||||
@ -1228,31 +1234,94 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
|
|
||||||
let beatingHearts: [UInt32] = [0x2764, 0x1F90E, 0x1F9E1, 0x1F499, 0x1F49A, 0x1F49C, 0x1F49B, 0x1F5A4, 0x1F90D]
|
let beatingHearts: [UInt32] = [0x2764, 0x1F90E, 0x1F9E1, 0x1F499, 0x1F49A, 0x1F49C, 0x1F49B, 0x1F5A4, 0x1F90D]
|
||||||
let peach = 0x1F351
|
let peach = 0x1F351
|
||||||
|
let coffin = 0x26B0
|
||||||
|
|
||||||
if let text = self.item?.message.text, let firstScalar = text.unicodeScalars.first, beatingHearts.contains(firstScalar.value) || firstScalar.value == peach {
|
let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|
||||||
return .optionalAction({
|
|> take(1)
|
||||||
let _ = startTime.start(next: { [weak self] time in
|
|> map { view in
|
||||||
guard let strongSelf = self else {
|
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
if let text = self.item?.message.text, let firstScalar = text.unicodeScalars.first {
|
||||||
var haptic: EmojiHaptic
|
if beatingHearts.contains(firstScalar.value) || firstScalar.value == peach {
|
||||||
if let current = strongSelf.haptic {
|
if shouldPlay {
|
||||||
haptic = current
|
animationNode.play()
|
||||||
} else {
|
}
|
||||||
if beatingHearts.contains(firstScalar.value) {
|
return .optionalAction({
|
||||||
haptic = HeartbeatHaptic()
|
let _ = startTime.start(next: { [weak self] time in
|
||||||
} else {
|
guard let strongSelf = self else {
|
||||||
haptic = PeachHaptic()
|
return
|
||||||
}
|
}
|
||||||
haptic.enabled = true
|
|
||||||
strongSelf.haptic = haptic
|
var haptic: EmojiHaptic
|
||||||
}
|
if let current = strongSelf.haptic {
|
||||||
if !haptic.active {
|
haptic = current
|
||||||
haptic.start(time: time)
|
} else {
|
||||||
|
if beatingHearts.contains(firstScalar.value) {
|
||||||
|
haptic = HeartbeatHaptic()
|
||||||
|
} else {
|
||||||
|
haptic = PeachHaptic()
|
||||||
|
}
|
||||||
|
haptic.enabled = true
|
||||||
|
strongSelf.haptic = haptic
|
||||||
|
}
|
||||||
|
if !haptic.active {
|
||||||
|
haptic.start(time: time)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return .optionalAction({
|
||||||
|
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)
|
||||||
|
mediaPlayer.togglePlayPause()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1512,3 +1581,39 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
self.contextSourceNode.contentNode.addSubnode(accessoryItemNode)
|
self.contextSourceNode.contentNode.addSubnode(accessoryItemNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AnimatedEmojiSoundsConfiguration {
|
||||||
|
static var defaultValue: AnimatedEmojiSoundsConfiguration {
|
||||||
|
return AnimatedEmojiSoundsConfiguration(sounds: [:])
|
||||||
|
}
|
||||||
|
|
||||||
|
public let sounds: [String: TelegramMediaFile]
|
||||||
|
|
||||||
|
fileprivate init(sounds: [String: TelegramMediaFile]) {
|
||||||
|
self.sounds = sounds
|
||||||
|
}
|
||||||
|
|
||||||
|
static func with(appConfiguration: AppConfiguration, account: Account) -> AnimatedEmojiSoundsConfiguration {
|
||||||
|
if let data = appConfiguration.data, let values = data["emojies_sounds"] as? [String: Any] {
|
||||||
|
var sounds: [String: TelegramMediaFile] = [:]
|
||||||
|
for (key, value) in values {
|
||||||
|
if let dict = value as? [String: String], var fileReferenceString = dict["file_reference_base64"] {
|
||||||
|
fileReferenceString = fileReferenceString.replacingOccurrences(of: "-", with: "+")
|
||||||
|
fileReferenceString = fileReferenceString.replacingOccurrences(of: "_", with: "/")
|
||||||
|
while fileReferenceString.count % 4 != 0 {
|
||||||
|
fileReferenceString.append("=")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let idString = dict["id"], let id = Int64(idString), let accessHashString = dict["access_hash"], let accessHash = Int64(accessHashString), let fileReference = Data(base64Encoded: fileReferenceString) {
|
||||||
|
let resource = CloudDocumentMediaResource(datacenterId: 1, fileId: id, accessHash: accessHash, size: nil, fileReference: fileReference, fileName: nil)
|
||||||
|
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: nil, attributes: [])
|
||||||
|
sounds[key] = file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AnimatedEmojiSoundsConfiguration(sounds: sounds)
|
||||||
|
} else {
|
||||||
|
return .defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -544,33 +544,34 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
|
|
||||||
self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
|
||||||
if let subnodes = self.subnodes {
|
func process(node: ASDisplayNode) {
|
||||||
for node in subnodes {
|
if node === self.accessoryItemNode {
|
||||||
if let contextNode = node as? ContextExtractedContentContainingNode {
|
return
|
||||||
if let contextSubnodes = contextNode.contentNode.subnodes {
|
}
|
||||||
inner: for contextSubnode in contextSubnodes {
|
|
||||||
if contextSubnode !== self.accessoryItemNode {
|
if node !== self {
|
||||||
if contextSubnode == self.backgroundNode {
|
switch node {
|
||||||
if self.backgroundNode.hasImage && self.backgroundWallpaperNode.hasImage {
|
case _ as ContextExtractedContentContainingNode, _ as ContextControllerSourceNode, _ as ContextExtractedContentNode:
|
||||||
continue inner
|
break
|
||||||
}
|
default:
|
||||||
}
|
|
||||||
contextSubnode.layer.allowsGroupOpacity = true
|
|
||||||
contextSubnode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak contextSubnode] _ in
|
|
||||||
contextSubnode?.layer.allowsGroupOpacity = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if node !== self.accessoryItemNode {
|
|
||||||
node.layer.allowsGroupOpacity = true
|
node.layer.allowsGroupOpacity = true
|
||||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
||||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak node] _ in
|
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak node] _ in
|
||||||
node?.layer.allowsGroupOpacity = false
|
node?.layer.allowsGroupOpacity = false
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let subnodes = node.subnodes else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for subnode in subnodes {
|
||||||
|
process(node: subnode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process(node: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
@ -588,10 +589,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||||
super.animateAdded(currentTimestamp, duration: duration)
|
super.animateAdded(currentTimestamp, duration: duration)
|
||||||
|
|
||||||
self.allowsGroupOpacity = true
|
if let subnodes = self.subnodes {
|
||||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in
|
for subnode in subnodes {
|
||||||
self?.allowsGroupOpacity = false
|
let layer = subnode.layer
|
||||||
})
|
layer.allowsGroupOpacity = true
|
||||||
|
layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak layer] _ in
|
||||||
|
layer?.allowsGroupOpacity = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -20,42 +20,6 @@ public enum PrefetchMediaItem {
|
|||||||
case animatedEmojiSticker(TelegramMediaFile)
|
case animatedEmojiSticker(TelegramMediaFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct AnimatedEmojiSoundsConfiguration {
|
|
||||||
static var defaultValue: AnimatedEmojiSoundsConfiguration {
|
|
||||||
return AnimatedEmojiSoundsConfiguration(sounds: [:])
|
|
||||||
}
|
|
||||||
|
|
||||||
public let sounds: [String: TelegramMediaFile]
|
|
||||||
|
|
||||||
fileprivate init(sounds: [String: TelegramMediaFile]) {
|
|
||||||
self.sounds = sounds
|
|
||||||
}
|
|
||||||
|
|
||||||
static func with(appConfiguration: AppConfiguration) -> AnimatedEmojiSoundsConfiguration {
|
|
||||||
if let data = appConfiguration.data, let values = data["emojies_sounds"] as? [String: Any] {
|
|
||||||
var sounds: [String: TelegramMediaFile] = [:]
|
|
||||||
for (key, value) in values {
|
|
||||||
if let dict = value as? [String: String], var fileReferenceString = dict["file_reference_base64"] {
|
|
||||||
fileReferenceString = fileReferenceString.replacingOccurrences(of: "-", with: "+")
|
|
||||||
fileReferenceString = fileReferenceString.replacingOccurrences(of: "_", with: "/")
|
|
||||||
while fileReferenceString.count % 4 != 0 {
|
|
||||||
fileReferenceString.append("=")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let idString = dict["id"], let id = Int64(idString), let accessHashString = dict["access_hash"], let accessHash = Int64(accessHashString), let fileReference = Data(base64Encoded: fileReferenceString) {
|
|
||||||
let resource = CloudDocumentMediaResource(datacenterId: 0, fileId: id, accessHash: accessHash, size: nil, fileReference: fileReference, fileName: nil)
|
|
||||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: nil, attributes: [])
|
|
||||||
sounds[key] = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return AnimatedEmojiSoundsConfiguration(sounds: sounds)
|
|
||||||
} else {
|
|
||||||
return .defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class PrefetchManagerImpl {
|
private final class PrefetchManagerImpl {
|
||||||
private let queue: Queue
|
private let queue: Queue
|
||||||
private let account: Account
|
private let account: Account
|
||||||
@ -89,12 +53,9 @@ private final class PrefetchManagerImpl {
|
|||||||
|
|
||||||
let orderedPreloadMedia = combineLatest(account.viewTracker.orderedPreloadMedia, loadedStickerPack(postbox: account.postbox, network: account.network, reference: .animatedEmoji, forceActualized: false), appConfiguration)
|
let orderedPreloadMedia = combineLatest(account.viewTracker.orderedPreloadMedia, loadedStickerPack(postbox: account.postbox, network: account.network, reference: .animatedEmoji, forceActualized: false), appConfiguration)
|
||||||
|> map { orderedPreloadMedia, stickerPack, appConfiguration -> [PrefetchMediaItem] in
|
|> map { orderedPreloadMedia, stickerPack, appConfiguration -> [PrefetchMediaItem] in
|
||||||
let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration)
|
let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: account)
|
||||||
let chatHistoryMediaItems = orderedPreloadMedia.map { PrefetchMediaItem.chatHistory($0) }
|
let chatHistoryMediaItems = orderedPreloadMedia.map { PrefetchMediaItem.chatHistory($0) }
|
||||||
var stickerItems: [PrefetchMediaItem] = []
|
var stickerItems: [PrefetchMediaItem] = []
|
||||||
|
|
||||||
var prefetchItems: [PrefetchMediaItem] = []
|
|
||||||
|
|
||||||
switch stickerPack {
|
switch stickerPack {
|
||||||
case let .result(_, items, _):
|
case let .result(_, items, _):
|
||||||
var animatedEmojiStickers: [String: StickerPackItem] = [:]
|
var animatedEmojiStickers: [String: StickerPackItem] = [:]
|
||||||
@ -113,11 +74,11 @@ private final class PrefetchManagerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stickerItems
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prefetchItems: [PrefetchMediaItem] = []
|
||||||
prefetchItems.append(contentsOf: chatHistoryMediaItems)
|
prefetchItems.append(contentsOf: chatHistoryMediaItems)
|
||||||
prefetchItems.append(contentsOf: stickerItems)
|
prefetchItems.append(contentsOf: stickerItems)
|
||||||
prefetchItems.append(contentsOf: emojiSounds.sounds.values.map { .animatedEmojiSticker($0) })
|
prefetchItems.append(contentsOf: emojiSounds.sounds.values.map { .animatedEmojiSticker($0) })
|
||||||
|
Loading…
x
Reference in New Issue
Block a user