diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index beacf3f56b..1a20f4e2c1 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -9,9 +9,9 @@ public enum DeviceLocationMode: Int32 { private final class DeviceLocationSubscriber { let id: Int32 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.mode = mode self.update = update @@ -39,7 +39,7 @@ public final class DeviceLocationManager: NSObject { private var subscribers: [DeviceLocationSubscriber] = [] private var currentTopMode: DeviceLocationMode? - private var currentLocation: (CLLocationCoordinate2D, Double)? + private var currentLocation: CLLocation? private var currentHeading: CLHeading? public init(queue: Queue, log: ((String) -> Void)? = nil) { @@ -56,12 +56,12 @@ public final class DeviceLocationManager: NSObject { } self.manager.delegate = self self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - self.manager.distanceFilter = 10.0 + self.manager.distanceFilter = 5.0 self.manager.activityType = .other 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()) let id = self.nextSubscriberId @@ -69,7 +69,7 @@ public final class DeviceLocationManager: NSObject { self.subscribers.append(DeviceLocationSubscriber(id: id, mode: mode, update: updated)) if let currentLocation = self.currentLocation { - updated(currentLocation.0, currentLocation.1, self.currentHeading?.magneticHeading) + updated(currentLocation, self.currentHeading?.magneticHeading) } 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 { public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { assert(self.queue.isCurrent()) if let location = locations.first { if self.currentTopMode != nil { - self.currentLocation = (location.coordinate, location.horizontalAccuracy) + self.currentLocation = location 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 if let currentLocation = self.currentLocation { 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 { return ( Signal { subscriber in - let disposable = manager.push(mode: .precise, updated: { coordinate, _, _ in - subscriber.putNext(coordinate) + let disposable = manager.push(mode: .precise, updated: { location, _ in + subscriber.putNext(location.coordinate) subscriber.putCompletion() }) return disposable diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 815b56521d..e9281d4c44 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -355,6 +355,8 @@ open class NavigationController: UINavigationController, ContainableController, } } + let initialPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden() + var overlayLayout = layout if let globalOverlayContainerParent = self.globalOverlayContainerParent { @@ -988,10 +990,15 @@ open class NavigationController: UINavigationController, ContainableController, self.isUpdatingContainers = false if notifyGlobalOverlayControllersUpdated { - self.globalOverlayControllersUpdated?() + self.internalGlobalOverlayControllersUpdated() } self.updateSupportedOrientations?() + + let updatedPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden() + if initialPrefersOnScreenNavigationHidden != updatedPrefersOnScreenNavigationHidden { + self.currentWindow?.invalidatePrefersOnScreenNavigationHidden() + } } private func controllerRemoved(_ controller: ViewController) { @@ -1184,7 +1191,7 @@ open class NavigationController: UINavigationController, ContainableController, if overlayContainer.controller === controller { overlayContainer.removeFromSupernode() strongSelf.globalOverlayContainers.remove(at: i) - strongSelf.globalOverlayControllersUpdated?() + strongSelf.internalGlobalOverlayControllersUpdated() break } } @@ -1194,6 +1201,7 @@ open class NavigationController: UINavigationController, ContainableController, if overlayContainer.controller === controller { overlayContainer.removeFromSupernode() strongSelf.overlayContainers.remove(at: i) + strongSelf.internalOverlayControllersUpdated() break } } @@ -1395,4 +1403,21 @@ open class NavigationController: UINavigationController, ContainableController, } } } + + private func internalGlobalOverlayControllersUpdated() { + self.globalOverlayControllersUpdated?() + self.currentWindow?.invalidatePrefersOnScreenNavigationHidden() + } + + private func internalOverlayControllersUpdated() { + self.currentWindow?.invalidatePrefersOnScreenNavigationHidden() + } + + private func collectPrefersOnScreenNavigationHidden() -> Bool { + var hidden = false + if let overlayController = self.topOverlayController { + hidden = hidden || overlayController.prefersOnScreenNavigationHidden + } + return hidden + } } diff --git a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/TwoAxisStepBarsChartController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/TwoAxisStepBarsChartController.swift index 0778a8875e..c1001173f0 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/TwoAxisStepBarsChartController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/TwoAxisStepBarsChartController.swift @@ -45,6 +45,9 @@ public class TwoAxisStepBarsChartController: BaseLinesChartController { private var prevoiusHorizontalStrideInterval: Int = 1 + public var hourly: Bool = false + public var min5: Bool = false + override public init(chartsCollection: ChartsCollection) { self.initialChartCollection = chartsCollection graphControllers = chartsCollection.chartValues.map { _ in GraphController() } @@ -252,8 +255,19 @@ public class TwoAxisStepBarsChartController: BaseLinesChartController { } func updateHorizontalLimits(horizontalRange: ClosedRange, 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, - scaleType: isZoomed ? .minutes5 : .day, + scaleType: scaleType, prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) { self.horizontalScalesRenderer.setup(labels: labels, animated: animated) self.prevoiusHorizontalStrideInterval = stride diff --git a/submodules/GraphUI/Sources/ChartNode.swift b/submodules/GraphUI/Sources/ChartNode.swift index 9559ba93c8..c461832c06 100644 --- a/submodules/GraphUI/Sources/ChartNode.swift +++ b/submodules/GraphUI/Sources/ChartNode.swift @@ -16,6 +16,8 @@ public enum ChartType { case step case twoAxisStep case hourlyStep + case twoAxisHourlyStep + case twoAxis5MinStep } public extension ChartTheme { @@ -90,6 +92,16 @@ public func createChartController(_ data: String, type: ChartType, getDetailsDat case .hourlyStep: controller = StepBarsChartController(chartsCollection: collection, hourly: true) 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 getDetailsData(date, { detailsData in diff --git a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift index 15f03860c7..4ef98100c4 100644 --- a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift +++ b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift @@ -107,9 +107,13 @@ public final class LiveLocationManagerImpl: LiveLocationManager { if let strongSelf = self { if value { 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 { - 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 { @@ -213,7 +217,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager { let ids = self.broadcastToMessageIds let remainingIds = Atomic>(value: Set(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 if let strongSelf = self { strongSelf.editMessageDisposables.set(nil, forKey: id) diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index fa3f6a043d..6c063fccc9 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -202,7 +202,6 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan private var disposable: Disposable? private var state: LocationViewState private let statePromise: Promise - private var geocodingDisposable = MetaDisposable() private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? private var listOffset: CGFloat? @@ -271,7 +270,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan return messages } - setupProximityNotificationImpl = { reset in + setupProximityNotificationImpl = { [weak self] reset in let _ = (liveLocations |> take(1) |> deliverOnMainQueue).start(next: { [weak self] messages in @@ -364,7 +363,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan 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 { entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, userLocation?.coordinate, beginTime, timeout)) } @@ -388,6 +387,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } 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 if let location = getLocation(from: message), let timeout = location.liveBroadcastingTimeout { liveBroadcastingTimeout = timeout @@ -536,13 +539,19 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan deinit { self.disposable?.dispose() - self.geocodingDisposable.dispose() self.locationManager.manager.stopUpdatingHeading() } func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { - self.headerNode.mapNode.userHeading = CGFloat(newHeading.magneticHeading) + if newHeading.headingAccuracy < 0.0 { + self.headerNode.mapNode.userHeading = nil + } + if newHeading.trueHeading > 0.0 { + self.headerNode.mapNode.userHeading = CGFloat(newHeading.trueHeading) + } else { + self.headerNode.mapNode.userHeading = CGFloat(newHeading.magneticHeading) + } } func updatePresentationData(_ presentationData: PresentationData) { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index 2f5fe0fe67..77649ecce7 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift @@ -385,8 +385,8 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon let forceUpdateLocation: () -> Void = { let locationCoordinates = Signal<(Double, Double), NoError> { subscriber in - return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.precise, updated: { coordinate, _, _ in - subscriber.putNext((coordinate.latitude, coordinate.longitude)) + return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.precise, updated: { location, _ in + subscriber.putNext((location.coordinate.latitude, location.coordinate.longitude)) subscriber.putCompletion() }) } diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index fd98f70c06..50dff5ee93 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -135,16 +135,15 @@ private enum StatsEntry: ItemListNodeEntry { }) }, sectionId: self.section, style: .blocks) case let .publicForward(_, _, _, _, message): - var views: Int = 0 + var views: Int32 = 0 for attribute in message.attributes { if let viewsAttribute = attribute as? ViewCountMessageAttribute { - views = viewsAttribute.count + views = Int32(viewsAttribute.count) break } } - var text: String = "" - text += "\(views) views" + let text: String = presentationData.strings.Stats_MessageViews(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: { arguments.openMessage(message.id) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil) @@ -161,7 +160,17 @@ private func messageStatsControllerEntries(data: MessageStats?, messages: Search if !data.interactionsGraph.isEmpty { 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 { diff --git a/submodules/TelegramCore/Sources/MessageStatistics.swift b/submodules/TelegramCore/Sources/MessageStatistics.swift index 1ee2e47aa9..2c4ec7e8cd 100644 --- a/submodules/TelegramCore/Sources/MessageStatistics.swift +++ b/submodules/TelegramCore/Sources/MessageStatistics.swift @@ -9,13 +9,13 @@ public struct MessageStats: Equatable { public let views: Int public let forwards: Int 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.forwards = forwards self.interactionsGraph = interactionsGraph - self.detailedInteractionsGraph = detailedInteractionsGraph + self.interactionsGraphDelta = interactionsGraphDelta } public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool { @@ -28,14 +28,14 @@ public struct MessageStats: Equatable { if lhs.interactionsGraph != rhs.interactionsGraph { return false } - if lhs.detailedInteractionsGraph != rhs.detailedInteractionsGraph { + if lhs.interactionsGraphDelta != rhs.interactionsGraphDelta { return false } return true } 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 in if case let .messageStats(apiViewsGraph) = result { let interactionsGraph = StatsGraph(apiStatsGraph: apiViewsGraph) - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - if case let .Loaded(tokenValue, _) = interactionsGraph, let token = tokenValue, Int64(message.timestamp + 60 * 60 * 24 * 2) > Int64(timestamp) { - return requestGraph(network: network, datacenterId: datacenterId, token: token, x: 1601596800000) - |> castError(MTRpcError.self) - |> map { detailedGraph -> MessageStats? in - return MessageStats(views: views, forwards: forwards, interactionsGraph: interactionsGraph, detailedInteractionsGraph: detailedGraph) + var interactionsGraphDelta: Int64 = 86400 + if case let .Loaded(_, data) = interactionsGraph { + if let start = data.range(of: "[\"x\",") { + let substring = data.suffix(from: start.upperBound) + if let end = substring.range(of: "],") { + 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 { return .single(nil) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index d46e2278ca..7e788fdbc8 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -445,9 +445,9 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, case let .geoProximityReached(fromId, toId, distance): let distanceString = stringForDistance(strings: strings, distance: Double(distance)) 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 { - 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: attributedString = nil diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 182fcddcea..f1f856a7ad 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -20,6 +20,7 @@ import Emoji import Markdown import ManagedAnimationNode import SlotMachineAnimationNode +import UniversalMediaPlayer private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) @@ -175,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 @@ -245,6 +248,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { deinit { self.disposable.dispose() + self.mediaStatusDisposable.set(nil) } required init?(coder aDecoder: NSCoder) { @@ -1217,7 +1221,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else if let _ = self.emojiFile { if let animationNode = self.animationNode as? AnimatedStickerNode { var startTime: Signal - if animationNode.playIfNeeded() { + var shouldPlay = false + if !animationNode.isPlaying { + shouldPlay = true startTime = .single(0.0) } else { startTime = animationNode.status @@ -1228,31 +1234,94 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let beatingHearts: [UInt32] = [0x2764, 0x1F90E, 0x1F9E1, 0x1F499, 0x1F49A, 0x1F49C, 0x1F49B, 0x1F5A4, 0x1F90D] 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 { - return .optionalAction({ - let _ = startTime.start(next: { [weak self] time in - guard let strongSelf = self else { - return - } - - var haptic: EmojiHaptic - if let current = strongSelf.haptic { - haptic = current - } else { - if beatingHearts.contains(firstScalar.value) { - haptic = HeartbeatHaptic() - } else { - haptic = PeachHaptic() + let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> take(1) + |> map { view in + return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue + } + + if let text = self.item?.message.text, let firstScalar = text.unicodeScalars.first { + if beatingHearts.contains(firstScalar.value) || firstScalar.value == peach { + if shouldPlay { + animationNode.play() + } + return .optionalAction({ + let _ = startTime.start(next: { [weak self] time in + guard let strongSelf = self else { + return } - haptic.enabled = true - strongSelf.haptic = haptic - } - if !haptic.active { - haptic.start(time: time) + + var haptic: EmojiHaptic + if let current = strongSelf.haptic { + haptic = current + } 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) } } + +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 + } + } +} diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 7550744ef6..6e15f77894 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -544,33 +544,34 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode self.shadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - if let subnodes = self.subnodes { - for node in subnodes { - if let contextNode = node as? ContextExtractedContentContainingNode { - if let contextSubnodes = contextNode.contentNode.subnodes { - inner: for contextSubnode in contextSubnodes { - if contextSubnode !== self.accessoryItemNode { - if contextSubnode == self.backgroundNode { - if self.backgroundNode.hasImage && self.backgroundWallpaperNode.hasImage { - continue inner - } - } - contextSubnode.layer.allowsGroupOpacity = true - contextSubnode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak contextSubnode] _ in - contextSubnode?.layer.allowsGroupOpacity = false - }) - } - } - } - } else if node !== self.accessoryItemNode { + func process(node: ASDisplayNode) { + if node === self.accessoryItemNode { + return + } + + if node !== self { + switch node { + case _ as ContextExtractedContentContainingNode, _ as ContextControllerSourceNode, _ as ContextExtractedContentNode: + break + default: node.layer.allowsGroupOpacity = true - node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak node] _ in node?.layer.allowsGroupOpacity = false }) + return } } + + guard let subnodes = node.subnodes else { + return + } + + for subnode in subnodes { + process(node: subnode) + } } + + process(node: self) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { @@ -588,10 +589,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode override func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) - self.allowsGroupOpacity = true - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in - self?.allowsGroupOpacity = false - }) + if let subnodes = self.subnodes { + for subnode in subnodes { + let layer = subnode.layer + layer.allowsGroupOpacity = true + layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak layer] _ in + layer?.allowsGroupOpacity = false + }) + } + } } override func didLoad() { diff --git a/submodules/TelegramUI/Sources/CoffinHaptic.swift b/submodules/TelegramUI/Sources/CoffinHaptic.swift new file mode 100644 index 0000000000..b1e0713ec3 --- /dev/null +++ b/submodules/TelegramUI/Sources/CoffinHaptic.swift @@ -0,0 +1,74 @@ +import Foundation +import Display +import SwiftSignalKit + +private let firstImpactTime: Double = 0.4 +private let secondImpactTime: Double = 0.6 + +final class CoffinHaptic: EmojiHaptic { + private var hapticFeedback = HapticFeedback() + private var timer: SwiftSignalKit.Timer? + private var time: Double = 0.0 + var enabled: Bool = false { + didSet { + if !self.enabled { + self.reset() + } + } + } + + var active: Bool { + return self.timer != nil + } + + private func reset() { + if let timer = self.timer { + self.time = 0.0 + timer.invalidate() + self.timer = nil + } + } + + private func beat(time: Double) { + let epsilon = 0.1 + if fabs(firstImpactTime - time) < epsilon || fabs(secondImpactTime - time) < epsilon { + self.hapticFeedback.impact(.heavy) + } + } + + func start(time: Double) { + self.hapticFeedback.prepareImpact() + + if time > firstImpactTime { + return + } + + let startTime: Double = 0.0 + + let block = { [weak self] in + guard let strongSelf = self, strongSelf.enabled else { + return + } + + strongSelf.time = startTime + strongSelf.beat(time: startTime) + strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in + guard let strongSelf = self, strongSelf.enabled else { + return + } + strongSelf.time += 0.2 + strongSelf.beat(time: strongSelf.time) + + if strongSelf.time > secondImpactTime { + strongSelf.reset() + strongSelf.time = 0.0 + strongSelf.timer?.invalidate() + strongSelf.timer = nil + } + }, queue: Queue.mainQueue()) + strongSelf.timer?.start() + } + + block() + } +} diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index 856176a275..6c0fa0a1e7 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -20,42 +20,6 @@ public enum PrefetchMediaItem { 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 let queue: Queue 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) |> 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) } var stickerItems: [PrefetchMediaItem] = [] - - var prefetchItems: [PrefetchMediaItem] = [] - switch stickerPack { case let .result(_, items, _): var animatedEmojiStickers: [String: StickerPackItem] = [:] @@ -113,11 +74,11 @@ private final class PrefetchManagerImpl { } } } - return stickerItems default: break } + var prefetchItems: [PrefetchMediaItem] = [] prefetchItems.append(contentsOf: chatHistoryMediaItems) prefetchItems.append(contentsOf: stickerItems) prefetchItems.append(contentsOf: emojiSounds.sounds.values.map { .animatedEmojiSticker($0) })