Reaction improvements

This commit is contained in:
Ali 2021-12-30 17:24:59 +04:00
parent dc0e3435fd
commit e00edd5f56
9 changed files with 288 additions and 135 deletions

View File

@ -10,6 +10,7 @@ public protocol LiveLocationSummaryManager {
public protocol LiveLocationManager {
var summaryManager: LiveLocationSummaryManager { get }
var isPolling: Signal<Bool, NoError> { get }
var hasBackgroundTasks: Signal<Bool, NoError> { get }
func cancelLiveLocation(peerId: EnginePeer.Id)
func pollOnce()

View File

@ -31,16 +31,60 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
var counter: Counter?
}
private struct AnimationState {
var fromCounter: Counter
var startTime: Double
var duration: Double
}
private var isExtracted: Bool = false
private var currentLayout: Layout?
private var animationState: AnimationState?
private var animator: ConstantDisplayLinkAnimator?
init() {
super.init(pointerStyle: nil)
}
func update(layout: Layout) {
if self.currentLayout != layout {
if let currentLayout = self.currentLayout, let counter = currentLayout.counter {
self.animationState = AnimationState(fromCounter: counter, startTime: CACurrentMediaTime(), duration: 0.15 * UIView.animationDurationFactor())
}
self.currentLayout = layout
self.updateBackgroundImage(animated: false)
self.updateAnimation()
}
}
private func updateAnimation() {
if let animationState = self.animationState {
let timestamp = CACurrentMediaTime()
if timestamp >= animationState.startTime + animationState.duration {
self.animationState = nil
}
}
if self.animationState != nil {
if self.animator == nil {
let animator = ConstantDisplayLinkAnimator(update: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateBackgroundImage(animated: false)
strongSelf.updateAnimation()
})
self.animator = animator
animator.isPaused = false
}
} else if let animator = self.animator {
animator.invalidate()
self.animator = nil
self.updateBackgroundImage(animated: false)
}
}
@ -87,10 +131,52 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
}
var textOrigin: CGFloat = size.width - counter.frame.width - 8.0 + floorToScreenPixels((counter.frame.width - totalComponentWidth) / 2.0)
for component in counter.components {
let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: foregroundColor)
string.draw(at: component.bounds.origin.offsetBy(dx: textOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0))
textOrigin += component.bounds.width
textOrigin = max(textOrigin, layout.baseSize.height / 2.0 + UIScreenPixel)
var rightTextOrigin = textOrigin + totalComponentWidth
let animationFraction: CGFloat
if let animationState = self.animationState {
animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration))
} else {
animationFraction = 1.0
}
for i in (0 ..< counter.components.count).reversed() {
let component = counter.components[i]
var componentAlpha: CGFloat = 1.0
var componentVerticalOffset: CGFloat = 0.0
if let animationState = self.animationState {
let reverseIndex = counter.components.count - 1 - i
if reverseIndex < animationState.fromCounter.components.count {
let previousComponent = animationState.fromCounter.components[animationState.fromCounter.components.count - 1 - reverseIndex]
if previousComponent != component {
componentAlpha = animationFraction
componentVerticalOffset = (1.0 - animationFraction) * 8.0
if previousComponent.string < component.string {
componentVerticalOffset = -componentVerticalOffset
}
let previousComponentAlpha = 1.0 - componentAlpha
var previousComponentVerticalOffset = -animationFraction * 8.0
if previousComponent.string < component.string {
previousComponentVerticalOffset = -previousComponentVerticalOffset
}
let componentOrigin = rightTextOrigin - previousComponent.bounds.width
let string = NSAttributedString(string: previousComponent.string, font: Font.medium(11.0), textColor: foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - previousComponentAlpha))
string.draw(at: previousComponent.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - previousComponent.bounds.height) / 2.0 + previousComponentVerticalOffset))
}
}
}
let componentOrigin = rightTextOrigin - component.bounds.width
let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - componentAlpha))
string.draw(at: component.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0 + componentVerticalOffset))
rightTextOrigin -= component.bounds.width
}
}
@ -120,7 +206,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
private static let maxDigitWidth: CGFloat = {
var maxWidth: CGFloat = 0.0
for i in 0 ..< 9 {
for i in 0 ... 9 {
let string = NSAttributedString(string: "\(i)", font: Font.medium(11.0), textColor: .black)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
maxWidth = max(maxWidth, boundingRect.width)
@ -151,13 +237,19 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
} else {
var resultSize = CGSize()
var resultComponents: [Component] = []
for component in spec.stringComponents {
for i in 0 ..< spec.stringComponents.count {
let component = spec.stringComponents[i]
let string = NSAttributedString(string: component, font: Font.medium(11.0), textColor: .black)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
resultComponents.append(Component(string: component, bounds: boundingRect))
if spec.stringComponents.count <= 2 {
resultSize.width += CounterLayout.maxDigitWidth
} else {
resultSize.width += boundingRect.width
}
resultSize.height = max(resultSize.height, boundingRect.height)
}
size = CGSize(width: ceil(resultSize.width), height: ceil(resultSize.height))
@ -243,75 +335,6 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
let imageFrame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
/*var previousDisplayCounter: String?
if let currentLayout = currentLayout {
if currentLayout.spec.component.avatarPeers.isEmpty {
previousDisplayCounter = countString(Int64(spec.component.count))
}
}
var currentDisplayCounter: String?
if spec.component.avatarPeers.isEmpty {
currentDisplayCounter = countString(Int64(spec.component.count))
}*/
/*let backgroundImage: UIImage
let extractedBackgroundImage: UIImage
if let currentLayout = currentLayout, currentLayout.spec.component.isSelected == spec.component.isSelected, currentLayout.spec.component.colors == spec.component.colors, previousDisplayCounter == currentDisplayCounter {
backgroundImage = currentLayout.backgroundImage
extractedBackgroundImage = currentLayout.extractedBackgroundImage
} else {
backgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor(argb: backgroundColor).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: height, height: height)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - height, y: 0.0), size: CGSize(width: height, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: height / 2.0, y: 0.0), size: CGSize(width: size.width - height, height: size.height)))
context.setBlendMode(.normal)
if let currentDisplayCounter = currentDisplayCounter {
let textColor = UIColor(argb: spec.component.isSelected ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground)
let string = NSAttributedString(string: currentDisplayCounter, font: Font.medium(11.0), textColor: textColor)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
if textColor.alpha < 1.0 {
context.setBlendMode(.copy)
}
string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0))
}
UIGraphicsPopContext()
})!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0))
extractedBackgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor(argb: spec.component.colors.extractedBackground).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: height, height: height)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - height, y: 0.0), size: CGSize(width: height, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: height / 2.0, y: 0.0), size: CGSize(width: size.width - height, height: size.height)))
context.setBlendMode(.normal)
if let currentDisplayCounter = currentDisplayCounter {
let textColor = UIColor(argb: spec.component.colors.extractedForeground)
let string = NSAttributedString(string: currentDisplayCounter, font: Font.medium(11.0), textColor: textColor)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
if textColor.alpha < 1.0 {
context.setBlendMode(.copy)
}
string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0))
}
UIGraphicsPopContext()
})!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0))
}*/
var counterLayout: CounterLayout?
var counterFrame: CGRect?

View File

@ -3,7 +3,8 @@ import CoreLocation
import SwiftSignalKit
public enum DeviceLocationMode: Int32 {
case precise = 0
case preciseForeground = 0
case preciseAlways = 1
}
private final class DeviceLocationSubscriber {
@ -51,15 +52,15 @@ public final class DeviceLocationManager: NSObject {
super.init()
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
self.manager.allowsBackgroundLocationUpdates = true
}
self.manager.delegate = self
self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self.manager.distanceFilter = 5.0
self.manager.activityType = .other
self.manager.pausesLocationUpdatesAutomatically = false
self.manager.headingFilter = 2.0
if #available(iOS 11.0, *) {
self.manager.showsBackgroundLocationIndicator = true
}
}
public func push(mode: DeviceLocationMode, updated: @escaping (CLLocation, Double?) -> Void) -> Disposable {
@ -108,6 +109,14 @@ public final class DeviceLocationManager: NSObject {
self.requestedAuthorization = true
self.manager.requestAlwaysAuthorization()
}
switch topMode {
case .preciseForeground:
self.manager.allowsBackgroundLocationUpdates = false
case .preciseAlways:
self.manager.allowsBackgroundLocationUpdates = true
}
self.manager.startUpdatingLocation()
self.manager.startUpdatingHeading()
}
@ -164,7 +173,7 @@ extension DeviceLocationManager: CLLocationManagerDelegate {
public func currentLocationManagerCoordinate(manager: DeviceLocationManager, timeout timeoutValue: Double) -> Signal<CLLocationCoordinate2D?, NoError> {
return (
Signal { subscriber in
let disposable = manager.push(mode: .precise, updated: { location, _ in
let disposable = manager.push(mode: .preciseForeground, updated: { location, _ in
subscriber.putNext(location.coordinate)
subscriber.putCompletion()
})

View File

@ -22,6 +22,11 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
public var isPolling: Signal<Bool, NoError> {
return self.pollingOnce.get()
}
public var hasBackgroundTasks: Signal<Bool, NoError> {
return self.hasActiveMessagesToBroadcast.get()
}
private let pollingOnce = ValuePromise<Bool>(false, ignoreRepeated: true)
private var pollingOnceValue = false {
didSet {
@ -92,6 +97,8 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
|> map { inForeground, hasActiveMessagesToBroadcast, pollingOnce -> Bool in
if (inForeground || pollingOnce) && hasActiveMessagesToBroadcast {
return true
} else if hasActiveMessagesToBroadcast {
return true
} else {
return false
}
@ -100,7 +107,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
|> deliverOn(self.queue)).start(next: { [weak self] value in
if let strongSelf = self {
if value {
strongSelf.deviceLocationDisposable.set(strongSelf.locationManager.push(mode: .precise, updated: { [weak self] location, heading in
strongSelf.deviceLocationDisposable.set(strongSelf.locationManager.push(mode: .preciseAlways, updated: { [weak self] location, heading in
self?.deviceLocationPromise.set(.single((location, heading)))
}))
} else {
@ -249,9 +256,9 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
}
public func pollOnce() {
if !self.broadcastToMessageIds.isEmpty {
/*if !self.broadcastToMessageIds.isEmpty {
self.pollingOnceValue = true
}
}*/
}
public func internalMessageForPeerId(_ peerId: EnginePeer.Id) -> EngineMessage.Id? {

View File

@ -179,6 +179,7 @@ final class ReactionNode: ASDisplayNode {
self.validSize = size
}
if self.animationNode == nil {
if isPreviewing {
if self.stillAnimationNode == nil {
let stillAnimationNode = AnimatedStickerNode()
@ -190,7 +191,7 @@ final class ReactionNode: ASDisplayNode {
stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
stillAnimationNode.updateLayout(size: animationFrame.size)
stillAnimationNode.started = { [weak self, weak stillAnimationNode] in
guard let strongSelf = self, let stillAnimationNode = stillAnimationNode, strongSelf.stillAnimationNode === stillAnimationNode else {
guard let strongSelf = self, let stillAnimationNode = stillAnimationNode, strongSelf.stillAnimationNode === stillAnimationNode, strongSelf.animationNode == nil else {
return
}
strongSelf.staticAnimationNode.alpha = 0.0
@ -232,6 +233,7 @@ final class ReactionNode: ASDisplayNode {
self.staticAnimationNode.alpha = 1.0
self.staticAnimationNode.layer.animateAlpha(from: previousAlpha, to: 1.0, duration: 0.08)
}
}
if !self.didSetupStillAnimation {
if self.animationNode == nil {

View File

@ -384,7 +384,7 @@ 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: { location, _ in
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in
subscriber.putNext((location.coordinate.latitude, location.coordinate.longitude))
subscriber.putCompletion()
})

View File

@ -866,7 +866,7 @@ public final class AccountViewTracker {
added = true
updatedReactions = attribute.withUpdatedResults(reactions)
if updatedReactions.reactions == attribute.reactions {
if updatedReactions == attribute {
return .skip
}
attributes[j] = updatedReactions

View File

@ -827,7 +827,13 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|> mapToSignal { context -> Signal<AccountRecordId?, NoError> in
if let context = context, let liveLocationManager = context.context.liveLocationManager {
let accountId = context.context.account.id
return liveLocationManager.isPolling
return combineLatest(queue: .mainQueue(),
liveLocationManager.isPolling,
liveLocationManager.hasBackgroundTasks
)
|> map { isPolling, hasBackgroundTasks -> Bool in
return isPolling || hasBackgroundTasks
}
|> distinctUntilChanged
|> map { value -> AccountRecordId? in
if value {

View File

@ -18,6 +18,7 @@ import AccountContext
import ChatInterfaceState
import ChatListUI
import ComponentFlow
import ReactionSelectionNode
extension ChatReplyThreadMessage {
var effectiveTopId: MessageId {
@ -2316,6 +2317,77 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let completion: (Bool, ListViewDisplayedItemRange) -> Void = { [weak self] wasTransformed, visibleRange in
if let strongSelf = self {
var newIncomingReactions: [MessageId: String] = [:]
if case let .peer(peerId) = strongSelf.chatLocation, peerId.namespace == Namespaces.Peer.CloudUser, let previousHistoryView = strongSelf.historyView {
var updatedIncomingReactions: [MessageId: String] = [:]
for entry in transition.historyView.filteredEntries {
switch entry {
case let .MessageEntry(message, _, _, _, _, _):
if message.flags.contains(.Incoming) {
continue
}
if let reactions = message.reactionsAttribute {
for reaction in reactions.reactions {
if !reaction.isSelected {
updatedIncomingReactions[message.id] = reaction.value
}
}
}
case let .MessageGroupEntry(_, messages, _):
for message in messages {
if message.0.flags.contains(.Incoming) {
continue
}
if let reactions = message.0.reactionsAttribute {
for reaction in reactions.reactions {
if !reaction.isSelected {
updatedIncomingReactions[message.0.id] = reaction.value
}
}
}
}
default:
break
}
}
for entry in previousHistoryView.filteredEntries {
switch entry {
case let .MessageEntry(message, _, _, _, _, _):
if let updatedReaction = updatedIncomingReactions[message.id] {
var previousReaction: String?
if let reactions = message.reactionsAttribute {
for reaction in reactions.reactions {
if !reaction.isSelected {
previousReaction = reaction.value
}
}
}
if previousReaction != updatedReaction {
newIncomingReactions[message.id] = updatedReaction
}
}
case let .MessageGroupEntry(_, messages, _):
for message in messages {
if let updatedReaction = updatedIncomingReactions[message.0.id] {
var previousReaction: String?
if let reactions = message.0.reactionsAttribute {
for reaction in reactions.reactions {
if !reaction.isSelected {
previousReaction = reaction.value
}
}
}
if previousReaction != updatedReaction {
newIncomingReactions[message.0.id] = updatedReaction
}
}
}
default:
break
}
}
}
strongSelf.historyView = transition.historyView
let loadState: ChatHistoryNodeLoadState
@ -2463,6 +2535,39 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
}
if !newIncomingReactions.isEmpty, let chatDisplayNode = strongSelf.controllerInteraction.chatControllerNode() as? ChatControllerNode {
strongSelf.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, let updatedReaction = newIncomingReactions[item.content.firstMessage.id], let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) {
for reaction in availableReactions.reactions {
if reaction.value == updatedReaction {
let standaloneReactionAnimation = StandaloneReactionAnimation()
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
chatDisplayNode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = chatDisplayNode.bounds
standaloneReactionAnimation.animateReactionSelection(
context: strongSelf.context,
theme: item.presentationData.theme.theme,
reaction: ReactionContextItem(
reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation,
listAnimation: reaction.activateAnimation,
applicationAnimation: reaction.effectAnimation
),
targetView: targetView,
hideNode: true,
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
}
)
}
}
}
}
}
strongSelf.hasActiveTransition = false
strongSelf.dequeueHistoryViewTransitions()
}