Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2021-12-23 13:18:41 +04:00
commit 7c7626f6cc
35 changed files with 653 additions and 219 deletions

View File

@ -7190,6 +7190,9 @@ Sorry for the inconvenience.";
"DoNotTranslate.Title" = "Do Not Translate"; "DoNotTranslate.Title" = "Do Not Translate";
"Channel.AdminLog.AllowedReactionsUpdated" = "%1$@ updated the list of allowed reactions to: %2$@";
"Channel.AdminLog.ReactionsDisabled" = "%1$@ disabled reactions";
"Contacts.ScanQrCode" = "Scan QR Code"; "Contacts.ScanQrCode" = "Scan QR Code";
"Contacts.QrCode.MyCode" = "My QR Code"; "Contacts.QrCode.MyCode" = "My QR Code";
"Contacts.QrCode.NoCodeFound" = "No valid QR code found in the image. Please try again."; "Contacts.QrCode.NoCodeFound" = "No valid QR code found in the image. Please try again.";

View File

@ -487,6 +487,12 @@ private func compressFrame(width: Int, height: Int, rgbData: Data) -> Data? {
} }
private final class AnimatedStickerDirectFrameSourceCache { private final class AnimatedStickerDirectFrameSourceCache {
private enum FrameRangeResult {
case range(Range<Int>)
case notFound
case corruptedFile
}
private let queue: Queue private let queue: Queue
private let storeQueue: Queue private let storeQueue: Queue
private let file: ManagedFileImpl private let file: ManagedFileImpl
@ -548,27 +554,31 @@ private final class AnimatedStickerDirectFrameSourceCache {
} }
} }
private func readFrameRange(index: Int) -> Range<Int>? { private func readFrameRange(index: Int) -> FrameRangeResult {
if index < 0 || index >= self.frameCount { if index < 0 || index >= self.frameCount {
return nil return .notFound
} }
self.file.seek(position: Int64(index * 4 * 2)) self.file.seek(position: Int64(index * 4 * 2))
var offset: Int32 = 0 var offset: Int32 = 0
var length: Int32 = 0 var length: Int32 = 0
if self.file.read(&offset, 4) != 4 { if self.file.read(&offset, 4) != 4 {
return nil return .corruptedFile
} }
if self.file.read(&length, 4) != 4 { if self.file.read(&length, 4) != 4 {
return nil return .corruptedFile
} }
if length == 0 { if length == 0 {
return nil return .notFound
} }
if length < 0 || offset < 0 { if length < 0 || offset < 0 {
return nil return .corruptedFile
} }
return (Int(offset) ..< Int(offset + length)) if Int64(offset) + Int64(length) > 100 * 1024 * 1024 {
return .corruptedFile
}
return .range(Int(offset) ..< Int(offset + length))
} }
func storeUncompressedRgbFrame(index: Int, rgbData: Data) { func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
@ -617,43 +627,52 @@ private final class AnimatedStickerDirectFrameSourceCache {
if index < 0 || index >= self.frameCount { if index < 0 || index >= self.frameCount {
return nil return nil
} }
guard let range = self.readFrameRange(index: index) else { let rangeResult = self.readFrameRange(index: index)
return nil
}
self.file.seek(position: Int64(range.lowerBound))
let length = range.upperBound - range.lowerBound
let compressedData = self.file.readData(count: length)
if compressedData.count != length {
return nil
}
var frameData: Data? switch rangeResult {
case let .range(range):
let decodeBufferLength = self.decodeBuffer.count self.file.seek(position: Int64(range.lowerBound))
let length = range.upperBound - range.lowerBound
compressedData.withUnsafeBytes { buffer -> Void in let compressedData = self.file.readData(count: length)
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { if compressedData.count != length {
return return nil
} }
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in var frameData: Data?
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
let decodeBufferLength = self.decodeBuffer.count
compressedData.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return return
} }
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return return
} }
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE) self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
frameData = Data(bytes: decodeBytes, count: resultLength) return
}
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
frameData = Data(bytes: decodeBytes, count: resultLength)
}
} }
} }
return frameData
case .notFound:
return nil
case .corruptedFile:
self.file.truncate(count: 0)
self.initializeFrameTable()
return nil
} }
return frameData
} }
} }

View File

@ -532,13 +532,28 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
public var updateDistractionFreeMode: ((Bool) -> Void)? public var updateDistractionFreeMode: ((Bool) -> Void)?
public var requestDismiss: (() -> Void)*/ public var requestDismiss: (() -> Void)*/
case let .animateOut(result, completion): case let .animateOut(result, completion):
let duration: Double = self.reactionContextNodeIsAnimatingOut ? 0.25 : 0.2 let duration: Double
let timingFunction: String
switch result {
case .default, .dismissWithoutContent:
duration = self.reactionContextNodeIsAnimatingOut ? 0.25 : 0.2
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
case let .custom(customTransition):
switch customTransition {
case let .animated(customDuration, curve):
duration = customDuration
timingFunction = curve.timingFunction
case .immediate:
duration = self.reactionContextNodeIsAnimatingOut ? 0.25 : 0.2
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
}
}
let putBackInfo = self.source.putBack() let putBackInfo = self.source.putBack()
if let putBackInfo = putBackInfo { if let putBackInfo = putBackInfo {
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, removeOnCompletion: false) self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
} }
let currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view) let currentContentScreenFrame = convertFrame(contentNode.containingNode.contentRect, from: contentNode.containingNode.view, to: self.view)
@ -574,7 +589,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
from: animationInContentDistance as NSNumber, from: animationInContentDistance as NSNumber,
to: 0.0 as NSNumber, to: 0.0 as NSNumber,
keyPath: "position.y", keyPath: "position.y",
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, timingFunction: timingFunction,
duration: duration, duration: duration,
delay: 0.0, delay: 0.0,
additive: true, additive: true,
@ -597,7 +612,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
from: 1.0 as NSNumber, from: 1.0 as NSNumber,
to: 0.01 as NSNumber, to: 0.01 as NSNumber,
keyPath: "transform.scale", keyPath: "transform.scale",
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, timingFunction: timingFunction,
duration: duration, duration: duration,
delay: 0.0, delay: 0.0,
removeOnCompletion: false removeOnCompletion: false
@ -611,7 +626,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
from: NSValue(cgPoint: CGPoint()), from: NSValue(cgPoint: CGPoint()),
to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)),
keyPath: "position", keyPath: "position",
timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, timingFunction: timingFunction,
duration: duration, duration: duration,
delay: 0.0, delay: 0.0,
removeOnCompletion: false, removeOnCompletion: false,

View File

@ -393,16 +393,16 @@ public extension ContainedViewLayoutTransition {
} }
} }
func animatePositionWithKeyframes(node: ASDisplayNode, keyframes: [AnyObject], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { func animatePositionWithKeyframes(node: ASDisplayNode, keyframes: [CGPoint], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
self.animatePositionWithKeyframes(layer: node.layer, keyframes: keyframes, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) self.animatePositionWithKeyframes(layer: node.layer, keyframes: keyframes, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
} }
func animatePositionWithKeyframes(layer: CALayer, keyframes: [AnyObject], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { func animatePositionWithKeyframes(layer: CALayer, keyframes: [CGPoint], removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self { switch self {
case .immediate: case .immediate:
completion?(true) completion?(true)
case let .animated(duration, curve): case let .animated(duration, curve):
layer.animateKeyframes(values: keyframes, duration: duration, keyPath: "position", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { value in layer.animateKeyframes(values: keyframes.map(NSValue.init(cgPoint:)), duration: duration, keyPath: "position", timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { value in
completion?(value) completion?(value)
}) })
} }

View File

@ -78,14 +78,14 @@ public final class JoinLinkPreviewController: ViewController {
if let strongSelf = self { if let strongSelf = self {
strongSelf.resolvedState = result strongSelf.resolvedState = result
switch result { switch result {
case let .invite(flags, title, about, photoRepresentation, participantsCount, participants): case let .invite(invite):
if flags.requestNeeded { if invite.flags.requestNeeded {
strongSelf.isRequest = true strongSelf.isRequest = true
strongSelf.isGroup = !flags.isBroadcast strongSelf.isGroup = !invite.flags.isBroadcast
strongSelf.controllerNode.setRequestPeer(image: photoRepresentation, title: title, about: about, memberCount: participantsCount, isGroup: !flags.isBroadcast) strongSelf.controllerNode.setRequestPeer(image: invite.photoRepresentation, title: invite.title, about: invite.about, memberCount: invite.participantsCount, isGroup: !invite.flags.isBroadcast)
} else { } else {
let data = JoinLinkPreviewData(isGroup: participants != nil, isJoined: false) let data = JoinLinkPreviewData(isGroup: invite.participants != nil, isJoined: false)
strongSelf.controllerNode.setInvitePeer(image: photoRepresentation, title: title, memberCount: participantsCount, members: participants?.map({ EnginePeer($0) }) ?? [], data: data) strongSelf.controllerNode.setInvitePeer(image: invite.photoRepresentation, title: invite.title, memberCount: invite.participantsCount, members: invite.participants?.map({ $0 }) ?? [], data: data)
} }
case let .alreadyJoined(peerId): case let .alreadyJoined(peerId):
strongSelf.navigateToPeer(peerId, nil) strongSelf.navigateToPeer(peerId, nil)

View File

@ -162,6 +162,9 @@ private func peerAllowedReactionListControllerEntries(
entries.append(.itemsHeader("AVAILABLE REACTIONS")) entries.append(.itemsHeader("AVAILABLE REACTIONS"))
var index = 0 var index = 0
for availableReaction in availableReactions.reactions { for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
entries.append(.item(index: index, value: availableReaction.value, file: availableReaction.staticIcon, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value))) entries.append(.item(index: index, value: availableReaction.value, file: availableReaction.staticIcon, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value)))
index += 1 index += 1
} }
@ -217,6 +220,9 @@ public func peerAllowedReactionListController(
if var updatedAllowedReactions = state.updatedAllowedReactions { if var updatedAllowedReactions = state.updatedAllowedReactions {
if updatedAllowedReactions.isEmpty { if updatedAllowedReactions.isEmpty {
for availableReaction in availableReactions.reactions { for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
updatedAllowedReactions.insert(availableReaction.value) updatedAllowedReactions.insert(availableReaction.value)
} }
} else { } else {

View File

@ -185,11 +185,16 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
self.backgroundLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.backgroundLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true)
self.backgroundLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) self.backgroundLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping)
self.backgroundShadowLayer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - size.width / 2.0, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true)
self.backgroundShadowLayer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping)
} }
func animateOut() { func animateOut() {
self.backgroundLayer.animateAlpha(from: CGFloat(self.backgroundLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) self.backgroundLayer.animateAlpha(from: CGFloat(self.backgroundLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false)
self.backgroundShadowLayer.animateAlpha(from: CGFloat(self.backgroundShadowLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false)
self.largeCircleLayer.animateAlpha(from: CGFloat(self.largeCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) self.largeCircleLayer.animateAlpha(from: CGFloat(self.largeCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false)
self.largeCircleShadowLayer.animateAlpha(from: CGFloat(self.largeCircleShadowLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false)
self.smallCircleLayer.animateAlpha(from: CGFloat(self.smallCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false) self.smallCircleLayer.animateAlpha(from: CGFloat(self.smallCircleLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false)
self.smallCircleShadowLayer.animateAlpha(from: CGFloat(self.smallCircleShadowLayer.opacity), to: 0.0, duration: 0.2, removeOnCompletion: false)
} }
} }

View File

@ -17,17 +17,20 @@ public final class ReactionContextItem {
} }
public let reaction: ReactionContextItem.Reaction public let reaction: ReactionContextItem.Reaction
public let appearAnimation: TelegramMediaFile
public let stillAnimation: TelegramMediaFile public let stillAnimation: TelegramMediaFile
public let listAnimation: TelegramMediaFile public let listAnimation: TelegramMediaFile
public let applicationAnimation: TelegramMediaFile public let applicationAnimation: TelegramMediaFile
public init( public init(
reaction: ReactionContextItem.Reaction, reaction: ReactionContextItem.Reaction,
appearAnimation: TelegramMediaFile,
stillAnimation: TelegramMediaFile, stillAnimation: TelegramMediaFile,
listAnimation: TelegramMediaFile, listAnimation: TelegramMediaFile,
applicationAnimation: TelegramMediaFile applicationAnimation: TelegramMediaFile
) { ) {
self.reaction = reaction self.reaction = reaction
self.appearAnimation = appearAnimation
self.stillAnimation = stillAnimation self.stillAnimation = stillAnimation
self.listAnimation = listAnimation self.listAnimation = listAnimation
self.applicationAnimation = applicationAnimation self.applicationAnimation = applicationAnimation
@ -38,6 +41,7 @@ private let largeCircleSize: CGFloat = 16.0
private let smallCircleSize: CGFloat = 8.0 private let smallCircleSize: CGFloat = 8.0
public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
private let theme: PresentationTheme private let theme: PresentationTheme
private let items: [ReactionContextItem] private let items: [ReactionContextItem]
@ -46,7 +50,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private let contentContainer: ASDisplayNode private let contentContainer: ASDisplayNode
private let contentContainerMask: UIImageView private let contentContainerMask: UIImageView
private let scrollNode: ASScrollNode private let scrollNode: ASScrollNode
private var itemNodes: [ReactionNode] = [] private let previewingItemContainer: ASDisplayNode
private var visibleItemNodes: [Int: ReactionNode] = [:]
private var isExpanded: Bool = true private var isExpanded: Bool = true
private var highlightedReaction: ReactionContextItem.Reaction? private var highlightedReaction: ReactionContextItem.Reaction?
@ -61,7 +66,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private weak var animationTargetView: UIView? private weak var animationTargetView: UIView?
private var animationHideNode: Bool = false private var animationHideNode: Bool = false
private var didAnimateIn: Bool = false
public init(context: AccountContext, theme: PresentationTheme, items: [ReactionContextItem]) { public init(context: AccountContext, theme: PresentationTheme, items: [ReactionContextItem]) {
self.context = context
self.theme = theme self.theme = theme
self.items = items self.items = items
@ -78,6 +86,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never self.scrollNode.view.contentInsetAdjustmentBehavior = .never
} }
self.previewingItemContainer = ASDisplayNode()
self.previewingItemContainer.isUserInteractionEnabled = false
self.contentContainer = ASDisplayNode() self.contentContainer = ASDisplayNode()
self.contentContainer.clipsToBounds = true self.contentContainer.clipsToBounds = true
self.contentContainer.addSubnode(self.scrollNode) self.contentContainer.addSubnode(self.scrollNode)
@ -113,12 +124,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
self.scrollNode.view.delegate = self self.scrollNode.view.delegate = self
self.itemNodes = self.items.map { item in
return ReactionNode(context: context, theme: theme, item: item)
}
self.itemNodes.forEach(self.scrollNode.addSubnode)
self.addSubnode(self.contentContainer) self.addSubnode(self.contentContainer)
self.addSubnode(self.previewingItemContainer)
} }
override public func didLoad() { override public func didLoad() {
@ -177,30 +184,76 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
private func updateScrolling(transition: ContainedViewLayoutTransition) { private func updateScrolling(transition: ContainedViewLayoutTransition) {
let sideInset: CGFloat = 14.0 let sideInset: CGFloat = 11.0
let minScale: CGFloat = 0.6 let itemSpacing: CGFloat = 9.0
let scaleDistance: CGFloat = 30.0 let itemSize: CGFloat = 40.0
let visibleBounds = self.scrollNode.view.bounds let verticalInset: CGFloat = 13.0
let rowHeight: CGFloat = 30.0
for itemNode in self.itemNodes { let visibleBounds = self.scrollNode.view.bounds
if itemNode.isExtracted { self.previewingItemContainer.bounds = visibleBounds
continue
} var validIndices = Set<Int>()
let itemScale: CGFloat for i in 0 ..< self.items.count {
let itemFrame = itemNode.frame.offsetBy(dx: -visibleBounds.minX, dy: 0.0) let columnIndex = i
if itemFrame.minX < sideInset || itemFrame.maxX > visibleBounds.width - sideInset { let column = CGFloat(columnIndex)
let edgeDistance: CGFloat
if itemFrame.minX < sideInset { let itemOffsetY: CGFloat = -1.0
edgeDistance = sideInset - itemFrame.minX
} else { let baseItemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize))
edgeDistance = itemFrame.maxX - (visibleBounds.width - sideInset) if visibleBounds.intersects(baseItemFrame) {
validIndices.insert(i)
var itemFrame = baseItemFrame
let isPreviewing = false
if self.highlightedReaction == self.items[i].reaction {
itemFrame = itemFrame.insetBy(dx: -4.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0)
//isPreviewing = true
}
var animateIn = false
let itemNode: ReactionNode
if let current = self.visibleItemNodes[i] {
itemNode = current
} else {
animateIn = self.didAnimateIn
itemNode = ReactionNode(context: self.context, theme: self.theme, item: self.items[i])
self.visibleItemNodes[i] = itemNode
self.scrollNode.addSubnode(itemNode)
}
if !itemNode.isExtracted {
if isPreviewing {
/*if itemNode.supernode !== self.previewingItemContainer {
self.previewingItemContainer.addSubnode(itemNode)
}*/
} else {
/*if itemNode.supernode !== self.scrollNode {
self.scrollNode.addSubnode(itemNode)
}*/
}
transition.updateFrame(node: itemNode, frame: itemFrame, beginWithCurrentState: true)
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, isPreviewing: isPreviewing, transition: transition)
if animateIn {
itemNode.animateIn()
}
} }
let edgeFactor: CGFloat = min(1.0, edgeDistance / scaleDistance)
itemScale = edgeFactor * minScale + (1.0 - edgeFactor) * 1.0
} else {
itemScale = 1.0
} }
transition.updateSublayerTransformScale(node: itemNode, scale: itemScale) }
var removedIndices: [Int] = []
for (index, itemNode) in self.visibleItemNodes {
if !validIndices.contains(index) {
removedIndices.append(index)
itemNode.removeFromSupernode()
}
}
for index in removedIndices {
self.visibleItemNodes.removeValue(forKey: index)
} }
} }
@ -232,24 +285,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
transition.updateFrame(node: self.contentContainer, frame: backgroundFrame) transition.updateFrame(node: self.contentContainer, frame: backgroundFrame)
transition.updateFrame(view: self.contentContainerMask, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.updateFrame(view: self.contentContainerMask, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.updateFrame(node: self.previewingItemContainer, frame: backgroundFrame)
self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: backgroundFrame.size.height) self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: backgroundFrame.size.height)
for i in 0 ..< self.items.count {
let columnIndex = i
let column = CGFloat(columnIndex)
let itemOffsetY: CGFloat = -1.0
var itemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize))
if self.highlightedReaction == self.items[i].reaction {
itemFrame = itemFrame.insetBy(dx: -6.0, dy: -6.0)
}
if !self.itemNodes[i].isExtracted {
transition.updateFrame(node: self.itemNodes[i], frame: itemFrame, beginWithCurrentState: true)
self.itemNodes[i].updateLayout(size: itemFrame.size, isExpanded: false, transition: transition)
}
}
self.updateScrolling(transition: transition) self.updateScrolling(transition: transition)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
@ -288,23 +326,28 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil) self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil)
} }
let mainCircleDuration: Double = 0.5 //let mainCircleDuration: Double = 0.5
let mainCircleDelay: Double = 0.1 let mainCircleDelay: Double = 0.1
self.backgroundNode.animateIn() self.backgroundNode.animateIn()
for i in 0 ..< self.itemNodes.count { self.didAnimateIn = true
let itemNode = self.itemNodes[i]
for i in 0 ..< self.items.count {
guard let itemNode = self.visibleItemNodes[i] else {
continue
}
let itemDelay = mainCircleDelay + 0.1 + Double(i) * 0.03 let itemDelay = mainCircleDelay + 0.1 + Double(i) * 0.03
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: itemDelay) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay, execute: { [weak itemNode] in
itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: itemDelay, initialVelocity: 0.0) itemNode?.animateIn()
})
} }
} }
public func animateOut(to targetAnchorRect: CGRect?, animatingOutToReaction: Bool) { public func animateOut(to targetAnchorRect: CGRect?, animatingOutToReaction: Bool) {
self.backgroundNode.animateOut() self.backgroundNode.animateOut()
for itemNode in self.itemNodes { for (_, itemNode) in self.visibleItemNodes {
if itemNode.isExtracted { if itemNode.isExtracted {
continue continue
} }
@ -364,7 +407,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
public func willAnimateOutToReaction(value: String) { public func willAnimateOutToReaction(value: String) {
for itemNode in self.itemNodes { for (_, itemNode) in self.visibleItemNodes {
if itemNode.item.reaction.rawValue != value { if itemNode.item.reaction.rawValue != value {
continue continue
} }
@ -373,7 +416,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
public func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { public func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
for itemNode in self.itemNodes { for (_, itemNode) in self.visibleItemNodes {
if itemNode.item.reaction.rawValue != value { if itemNode.item.reaction.rawValue != value {
continue continue
} }
@ -397,7 +440,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view)
let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) let selfTargetRect = self.view.convert(targetView.bounds, from: targetView)
let expandedScale: CGFloat = 3.0 let expandedScale: CGFloat = 4.0
let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale)) let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale))
var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize)
@ -408,10 +451,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
self.addSubnode(itemNode) self.addSubnode(itemNode)
itemNode.frame = selfSourceRect //itemNode.position = selfSourceRect.center
itemNode.position = expandedFrame.center itemNode.position = expandedFrame.center
transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size)) transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size))
itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: transition) itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, isPreviewing: false, transition: transition)
transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0)) transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0))
let additionalAnimationNode = AnimatedStickerNode() let additionalAnimationNode = AnimatedStickerNode()
@ -504,7 +548,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
public func reaction(at point: CGPoint) -> ReactionContextItem? { public func reaction(at point: CGPoint) -> ReactionContextItem? {
for i in 0 ..< 2 { for i in 0 ..< 2 {
let touchInset: CGFloat = i == 0 ? 0.0 : 8.0 let touchInset: CGFloat = i == 0 ? 0.0 : 8.0
for itemNode in self.itemNodes { for (_, itemNode) in self.visibleItemNodes {
if itemNode.supernode === self.scrollNode && !self.scrollNode.bounds.intersects(itemNode.frame) {
continue
}
let itemPoint = self.view.convert(point, to: itemNode.view) let itemPoint = self.view.convert(point, to: itemNode.view)
if itemNode.bounds.insetBy(dx: -touchInset, dy: -touchInset).contains(itemPoint) { if itemNode.bounds.insetBy(dx: -touchInset, dy: -touchInset).contains(itemPoint) {
return itemNode.item return itemNode.item
@ -515,7 +562,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
public func performReactionSelection(reaction: ReactionContextItem.Reaction) { public func performReactionSelection(reaction: ReactionContextItem.Reaction) {
for itemNode in self.itemNodes { for (_, itemNode) in self.visibleItemNodes {
if itemNode.item.reaction == reaction { if itemNode.item.reaction == reaction {
self.reactionSelected?(itemNode.item) self.reactionSelected?(itemNode.item)
break break
@ -609,7 +656,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
self.addSubnode(itemNode) self.addSubnode(itemNode)
itemNode.frame = expandedFrame itemNode.frame = expandedFrame
itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, transition: .immediate) itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, isPreviewing: false, transition: .immediate)
itemNode.layer.animateSpring(from: (selfTargetRect.width / expandedFrame.width) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) itemNode.layer.animateSpring(from: (selfTargetRect.width / expandedFrame.width) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04) itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04)
@ -779,7 +826,7 @@ public final class StandaloneDismissReactionAnimation: ASDisplayNode {
} }
} }
private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [AnyObject] { private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] {
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation) let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
let x1 = sourcePoint.x let x1 = sourcePoint.x
@ -789,13 +836,13 @@ private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to tar
let x3 = targetPosition.x let x3 = targetPosition.x
let y3 = targetPosition.y let y3 = targetPosition.y
var keyframes: [AnyObject] = [] var keyframes: [CGPoint] = []
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 { if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
for i in 0 ..< 10 { for i in 0 ..< 10 {
let k = CGFloat(i) / CGFloat(10 - 1) let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) keyframes.append(CGPoint(x: x, y: y))
} }
} else { } else {
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
@ -806,7 +853,7 @@ private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to tar
let k = CGFloat(i) / CGFloat(10 - 1) let k = CGFloat(i) / CGFloat(10 - 1)
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
let y = a * x * x + b * x + c let y = a * x * x + b * x + c
keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) keyframes.append(CGPoint(x: x, y: y))
} }
} }

View File

@ -38,10 +38,14 @@ private let font = Font.medium(13.0)
final class ReactionNode: ASDisplayNode { final class ReactionNode: ASDisplayNode {
let context: AccountContext let context: AccountContext
let item: ReactionContextItem let item: ReactionContextItem
private let staticImageNode: TransformImageNode
private let stillAnimationNode: AnimatedStickerNode private var animateInAnimationNode: AnimatedStickerNode?
private let staticAnimationNode: AnimatedStickerNode
private var stillAnimationNode: AnimatedStickerNode?
private var animationNode: AnimatedStickerNode? private var animationNode: AnimatedStickerNode?
private var dismissedStillAnimationNodes: [AnimatedStickerNode] = []
private var fetchStickerDisposable: Disposable? private var fetchStickerDisposable: Disposable?
private var fetchFullAnimationDisposable: Disposable? private var fetchFullAnimationDisposable: Disposable?
@ -55,22 +59,31 @@ final class ReactionNode: ASDisplayNode {
self.context = context self.context = context
self.item = item self.item = item
self.staticImageNode = TransformImageNode() self.staticAnimationNode = AnimatedStickerNode()
self.stillAnimationNode = AnimatedStickerNode() self.staticAnimationNode.isHidden = true
self.animateInAnimationNode = AnimatedStickerNode()
super.init() super.init()
self.addSubnode(self.staticImageNode) if let animateInAnimationNode = self.animateInAnimationNode {
self.addSubnode(animateInAnimationNode)
}
self.addSubnode(self.staticAnimationNode)
self.addSubnode(self.stillAnimationNode) self.animateInAnimationNode?.completed = { [weak self] _ in
self.stillAnimationNode.started = { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.staticImageNode.isHidden = true if strongSelf.animationNode == nil {
strongSelf.staticAnimationNode.isHidden = false
}
strongSelf.animateInAnimationNode?.removeFromSupernode()
strongSelf.animateInAnimationNode = nil
} }
self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.appearAnimation.resource)).start()
self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.stillAnimation.resource)).start() self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.stillAnimation.resource)).start()
self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.listAnimation.resource)).start() self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.listAnimation.resource)).start()
self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.applicationAnimation.resource)).start() self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .standalone(resource: item.applicationAnimation.resource)).start()
@ -81,7 +94,11 @@ final class ReactionNode: ASDisplayNode {
self.fetchFullAnimationDisposable?.dispose() self.fetchFullAnimationDisposable?.dispose()
} }
func updateLayout(size: CGSize, isExpanded: Bool, transition: ContainedViewLayoutTransition) { func animateIn() {
self.animateInAnimationNode?.visibility = true
}
func updateLayout(size: CGSize, isExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) {
let intrinsicSize = size let intrinsicSize = size
let animationSize = self.item.stillAnimation.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) let animationSize = self.item.stillAnimation.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
@ -101,60 +118,139 @@ final class ReactionNode: ASDisplayNode {
self.animationNode = animationNode self.animationNode = animationNode
self.addSubnode(animationNode) self.addSubnode(animationNode)
animationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.staticImageNode.isHidden = true
}
animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.listAnimation.resource.id))) animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.listAnimation.resource.id)))
animationNode.frame = animationFrame animationNode.frame = animationFrame
animationNode.updateLayout(size: animationFrame.size) animationNode.updateLayout(size: animationFrame.size)
if transition.isAnimated, !self.staticImageNode.frame.isEmpty {
transition.animateTransformScale(node: animationNode, from: self.staticImageNode.bounds.width / animationFrame.width)
transition.animatePositionAdditive(node: animationNode, offset: CGPoint(x: self.staticImageNode.frame.midX - animationFrame.midX, y: self.staticImageNode.frame.midY - animationFrame.midY))
}
animationNode.visibility = true
self.stillAnimationNode.alpha = 0.0
if transition.isAnimated { if transition.isAnimated {
self.stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in if let stillAnimationNode = self.stillAnimationNode, !stillAnimationNode.frame.isEmpty {
self?.stillAnimationNode.visibility = false stillAnimationNode.alpha = 0.0
}) stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
guard let strongSelf = self, let stillAnimationNode = strongSelf.stillAnimationNode else {
return
}
strongSelf.stillAnimationNode = nil
stillAnimationNode.removeFromSupernode()
})
}
if let animateInAnimationNode = self.animateInAnimationNode {
animateInAnimationNode.alpha = 0.0
animateInAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
guard let strongSelf = self, let animateInAnimationNode = strongSelf.animateInAnimationNode else {
return
}
strongSelf.animateInAnimationNode = nil
animateInAnimationNode.removeFromSupernode()
})
}
var referenceNode: ASDisplayNode?
if let animateInAnimationNode = self.animateInAnimationNode {
referenceNode = animateInAnimationNode
} else if !self.staticAnimationNode.isHidden {
referenceNode = self.staticAnimationNode
}
if let referenceNode = referenceNode {
transition.animateTransformScale(node: animationNode, from: referenceNode.bounds.width / animationFrame.width)
transition.animatePositionAdditive(node: animationNode, offset: CGPoint(x: referenceNode.frame.midX - animationFrame.midX, y: referenceNode.frame.midY - animationFrame.midY))
}
if !self.staticAnimationNode.isHidden {
transition.animateTransformScale(node: self.staticAnimationNode, from: self.staticAnimationNode.bounds.width / animationFrame.width)
transition.animatePositionAdditive(node: self.staticAnimationNode, offset: CGPoint(x: self.staticAnimationNode.frame.midX - animationFrame.midX, y: self.staticAnimationNode.frame.midY - animationFrame.midY))
self.staticAnimationNode.alpha = 0.0
self.staticAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} else { } else {
self.stillAnimationNode.visibility = false if let stillAnimationNode = self.stillAnimationNode {
self.stillAnimationNode = nil
stillAnimationNode.removeFromSupernode()
}
self.staticAnimationNode.isHidden = true
} }
animationNode.visibility = true
} }
if self.validSize != size { if self.validSize != size {
self.validSize = size self.validSize = size
}
if !self.staticImageNode.isHidden {
self.staticImageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: item.stillAnimation, small: false, size: CGSize(width: animationDisplaySize.width * UIScreenScale, height: animationDisplaySize.height * UIScreenScale), fitzModifier: nil, fetched: false, onlyFullSize: false, thumbnail: false, synchronousLoad: false)) if isPreviewing {
let imageApply = self.staticImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: animationDisplaySize, boundingSize: animationDisplaySize, intrinsicInsets: UIEdgeInsets())) if self.stillAnimationNode == nil {
imageApply() let stillAnimationNode = AnimatedStickerNode()
self.stillAnimationNode = stillAnimationNode
self.addSubnode(stillAnimationNode)
stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
stillAnimationNode.position = animationFrame.center
stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
stillAnimationNode.updateLayout(size: animationFrame.size)
stillAnimationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.staticAnimationNode.alpha = 0.0
}
stillAnimationNode.visibility = true
transition.animateTransformScale(node: stillAnimationNode, from: self.staticAnimationNode.bounds.width / animationFrame.width)
transition.animatePositionAdditive(node: stillAnimationNode, offset: CGPoint(x: self.staticAnimationNode.frame.midX - animationFrame.midX, y: self.staticAnimationNode.frame.midY - animationFrame.midY))
} else {
if let stillAnimationNode = self.stillAnimationNode {
transition.updatePosition(node: stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: stillAnimationNode, scale: animationFrame.size.width / stillAnimationNode.bounds.width, beginWithCurrentState: true)
}
} }
transition.updateFrame(node: self.staticImageNode, frame: animationFrame) } else if let stillAnimationNode = self.stillAnimationNode {
self.stillAnimationNode = nil
self.dismissedStillAnimationNodes.append(stillAnimationNode)
transition.updatePosition(node: stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: stillAnimationNode, scale: animationFrame.size.width / stillAnimationNode.bounds.width, beginWithCurrentState: true)
stillAnimationNode.alpha = 0.0
stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.14, completion: { [weak self, weak stillAnimationNode] _ in
guard let strongSelf = self, let stillAnimationNode = stillAnimationNode else {
return
}
stillAnimationNode.removeFromSupernode()
strongSelf.dismissedStillAnimationNodes.removeAll(where: { $0 === stillAnimationNode })
})
let previousAlpha = CGFloat(self.staticAnimationNode.layer.presentation()?.opacity ?? self.staticAnimationNode.layer.opacity)
self.staticAnimationNode.alpha = 1.0
self.staticAnimationNode.layer.animateAlpha(from: previousAlpha, to: 1.0, duration: 0.08)
} }
if !self.didSetupStillAnimation { if !self.didSetupStillAnimation {
if self.animationNode == nil { if self.animationNode == nil {
self.didSetupStillAnimation = true self.didSetupStillAnimation = true
self.stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.5), height: Int(animationDisplaySize.height * 2.5), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
self.stillAnimationNode.position = animationFrame.center self.staticAnimationNode.position = animationFrame.center
self.stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) self.staticAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
self.stillAnimationNode.updateLayout(size: animationFrame.size) self.staticAnimationNode.updateLayout(size: animationFrame.size)
self.stillAnimationNode.visibility = true self.staticAnimationNode.visibility = true
if let animateInAnimationNode = self.animateInAnimationNode {
animateInAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.appearAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.appearAnimation.resource.id)))
animateInAnimationNode.position = animationFrame.center
animateInAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
animateInAnimationNode.updateLayout(size: animationFrame.size)
}
} }
} else { } else {
transition.updatePosition(node: self.stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true) transition.updatePosition(node: self.staticAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: self.stillAnimationNode, scale: animationFrame.size.width / self.stillAnimationNode.bounds.width, beginWithCurrentState: true) transition.updateTransformScale(node: self.staticAnimationNode, scale: animationFrame.size.width / self.staticAnimationNode.bounds.width, beginWithCurrentState: true)
if let animateInAnimationNode = self.animateInAnimationNode {
transition.updatePosition(node: animateInAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: animateInAnimationNode, scale: animationFrame.size.width / animateInAnimationNode.bounds.width, beginWithCurrentState: true)
}
} }
} }
func didAppear() {
}
} }

View File

@ -193,6 +193,10 @@ private func quickReactionSetupControllerEntries(
entries.append(.itemsHeader("QUICK REACTION")) entries.append(.itemsHeader("QUICK REACTION"))
var index = 0 var index = 0
for availableReaction in availableReactions.reactions { for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
entries.append(.item( entries.append(.item(
index: index, index: index,
value: availableReaction.value, value: availableReaction.value,
@ -226,12 +230,8 @@ public func quickReactionSetupController(
let arguments = QuickReactionSetupControllerArguments( let arguments = QuickReactionSetupControllerArguments(
context: context, context: context,
selectItem: { reaction in selectItem: { reaction in
let _ = updateReactionSettingsInteractively(postbox: context.account.postbox, { settings in let _ = context.engine.stickers.updateQuickReaction(reaction: reaction).start()
var settings = settings
settings.quickReaction = reaction
return settings
}).start()
} }
) )
@ -252,6 +252,10 @@ public func quickReactionSetupController(
if let availableReactions = availableReactions { if let availableReactions = availableReactions {
for availableReaction in availableReactions.reactions { for availableReaction in availableReactions.reactions {
if !availableReaction.isEnabled {
continue
}
let signal: Signal<(String, UIImage?), NoError> = context.account.postbox.mediaBox.resourceData(availableReaction.staticIcon.resource) let signal: Signal<(String, UIImage?), NoError> = context.account.postbox.mediaBox.resourceData(availableReaction.staticIcon.resource)
|> distinctUntilChanged(isEqual: { lhs, rhs in |> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs.complete == rhs.complete return lhs.complete == rhs.complete

View File

@ -148,6 +148,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
standaloneReactionAnimation.animateReactionSelection( standaloneReactionAnimation.animateReactionSelection(
context: item.context, theme: item.theme, reaction: ReactionContextItem( context: item.context, theme: item.theme, reaction: ReactionContextItem(
reaction: ReactionContextItem.Reaction(rawValue: reaction.value), reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation, stillAnimation: reaction.selectAnimation,
listAnimation: reaction.activateAnimation, listAnimation: reaction.activateAnimation,
applicationAnimation: reaction.effectAnimation applicationAnimation: reaction.effectAnimation

View File

@ -480,6 +480,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1347021750] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByRequest($0) } dict[-1347021750] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByRequest($0) }
dict[-886388890] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleNoForwards($0) } dict[-886388890] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleNoForwards($0) }
dict[663693416] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionSendMessage($0) } dict[663693416] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionSendMessage($0) }
dict[-1661470870] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionChangeAvailableReactions($0) }
dict[-1271602504] = { return Api.auth.ExportedAuthorization.parse_exportedAuthorization($0) } dict[-1271602504] = { return Api.auth.ExportedAuthorization.parse_exportedAuthorization($0) }
dict[2103482845] = { return Api.SecurePlainData.parse_securePlainPhone($0) } dict[2103482845] = { return Api.SecurePlainData.parse_securePlainPhone($0) }
dict[569137759] = { return Api.SecurePlainData.parse_securePlainEmail($0) } dict[569137759] = { return Api.SecurePlainData.parse_securePlainEmail($0) }
@ -791,7 +792,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) } dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) }
dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) } dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) }
dict[-1449145777] = { return Api.upload.CdnFile.parse_cdnFile($0) } dict[-1449145777] = { return Api.upload.CdnFile.parse_cdnFile($0) }
dict[1424116867] = { return Api.AvailableReaction.parse_availableReaction($0) } dict[35486795] = { return Api.AvailableReaction.parse_availableReaction($0) }
dict[415997816] = { return Api.help.InviteText.parse_inviteText($0) } dict[415997816] = { return Api.help.InviteText.parse_inviteText($0) }
dict[-1826077446] = { return Api.MessageUserReaction.parse_messageUserReaction($0) } dict[-1826077446] = { return Api.MessageUserReaction.parse_messageUserReaction($0) }
dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) } dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) }

View File

@ -11695,6 +11695,7 @@ public extension Api {
case channelAdminLogEventActionParticipantJoinByRequest(invite: Api.ExportedChatInvite, approvedBy: Int64) case channelAdminLogEventActionParticipantJoinByRequest(invite: Api.ExportedChatInvite, approvedBy: Int64)
case channelAdminLogEventActionToggleNoForwards(newValue: Api.Bool) case channelAdminLogEventActionToggleNoForwards(newValue: Api.Bool)
case channelAdminLogEventActionSendMessage(message: Api.Message) case channelAdminLogEventActionSendMessage(message: Api.Message)
case channelAdminLogEventActionChangeAvailableReactions(prevValue: [String], newValue: [String])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
@ -11923,6 +11924,21 @@ public extension Api {
} }
message.serialize(buffer, true) message.serialize(buffer, true)
break break
case .channelAdminLogEventActionChangeAvailableReactions(let prevValue, let newValue):
if boxed {
buffer.appendInt32(-1661470870)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(prevValue.count))
for item in prevValue {
serializeString(item, buffer: buffer, boxed: false)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(newValue.count))
for item in newValue {
serializeString(item, buffer: buffer, boxed: false)
}
break
} }
} }
@ -11998,6 +12014,8 @@ public extension Api {
return ("channelAdminLogEventActionToggleNoForwards", [("newValue", newValue)]) return ("channelAdminLogEventActionToggleNoForwards", [("newValue", newValue)])
case .channelAdminLogEventActionSendMessage(let message): case .channelAdminLogEventActionSendMessage(let message):
return ("channelAdminLogEventActionSendMessage", [("message", message)]) return ("channelAdminLogEventActionSendMessage", [("message", message)])
case .channelAdminLogEventActionChangeAvailableReactions(let prevValue, let newValue):
return ("channelAdminLogEventActionChangeAvailableReactions", [("prevValue", prevValue), ("newValue", newValue)])
} }
} }
@ -12485,6 +12503,24 @@ public extension Api {
return nil return nil
} }
} }
public static func parse_channelAdminLogEventActionChangeAvailableReactions(_ reader: BufferReader) -> ChannelAdminLogEventAction? {
var _1: [String]?
if let _ = reader.readInt32() {
_1 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self)
}
var _2: [String]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionChangeAvailableReactions(prevValue: _1!, newValue: _2!)
}
else {
return nil
}
}
} }
public enum SecurePlainData: TypeConstructorDescription { public enum SecurePlainData: TypeConstructorDescription {
@ -20200,14 +20236,15 @@ public extension Api {
} }
public enum AvailableReaction: TypeConstructorDescription { public enum AvailableReaction: TypeConstructorDescription {
case availableReaction(reaction: String, title: String, staticIcon: Api.Document, appearAnimation: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document) case availableReaction(flags: Int32, reaction: String, title: String, staticIcon: Api.Document, appearAnimation: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .availableReaction(let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation): case .availableReaction(let flags, let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation):
if boxed { if boxed {
buffer.appendInt32(1424116867) buffer.appendInt32(35486795)
} }
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(reaction, buffer: buffer, boxed: false) serializeString(reaction, buffer: buffer, boxed: false)
serializeString(title, buffer: buffer, boxed: false) serializeString(title, buffer: buffer, boxed: false)
staticIcon.serialize(buffer, true) staticIcon.serialize(buffer, true)
@ -20221,20 +20258,18 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .availableReaction(let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation): case .availableReaction(let flags, let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation):
return ("availableReaction", [("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("appearAnimation", appearAnimation), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation)]) return ("availableReaction", [("flags", flags), ("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("appearAnimation", appearAnimation), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation)])
} }
} }
public static func parse_availableReaction(_ reader: BufferReader) -> AvailableReaction? { public static func parse_availableReaction(_ reader: BufferReader) -> AvailableReaction? {
var _1: String? var _1: Int32?
_1 = parseString(reader) _1 = reader.readInt32()
var _2: String? var _2: String?
_2 = parseString(reader) _2 = parseString(reader)
var _3: Api.Document? var _3: String?
if let signature = reader.readInt32() { _3 = parseString(reader)
_3 = Api.parse(reader, signature: signature) as? Api.Document
}
var _4: Api.Document? var _4: Api.Document?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.Document _4 = Api.parse(reader, signature: signature) as? Api.Document
@ -20251,6 +20286,10 @@ public extension Api {
if let signature = reader.readInt32() { if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.Document _7 = Api.parse(reader, signature: signature) as? Api.Document
} }
var _8: Api.Document?
if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.Document
}
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -20258,8 +20297,9 @@ public extension Api {
let _c5 = _5 != nil let _c5 = _5 != nil
let _c6 = _6 != nil let _c6 = _6 != nil
let _c7 = _7 != nil let _c7 = _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { let _c8 = _8 != nil
return Api.AvailableReaction.availableReaction(reaction: _1!, title: _2!, staticIcon: _3!, appearAnimation: _4!, selectAnimation: _5!, activateAnimation: _6!, effectAnimation: _7!) if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.AvailableReaction.availableReaction(flags: _1!, reaction: _2!, title: _3!, staticIcon: _4!, appearAnimation: _5!, selectAnimation: _6!, activateAnimation: _7!, effectAnimation: _8!)
} }
else { else {
return nil return nil

View File

@ -4592,11 +4592,11 @@ public extension Api {
}) })
} }
public static func setDefaultReaction(emoji: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) { public static func setDefaultReaction(reaction: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(1474910882) buffer.appendInt32(-647969580)
serializeString(emoji, buffer: buffer, boxed: false) serializeString(reaction, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.setDefaultReaction", parameters: [("emoji", emoji)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in return (FunctionDescription(name: "messages.setDefaultReaction", parameters: [("reaction", reaction)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer) let reader = BufferReader(buffer)
var result: Api.Bool? var result: Api.Bool?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {

View File

@ -2,14 +2,17 @@ import Foundation
import Postbox import Postbox
public final class AdMessageAttribute: MessageAttribute { public final class AdMessageAttribute: MessageAttribute {
public enum MessageTarget {
case peer(id: EnginePeer.Id, message: EngineMessage.Id?, startParam: String?)
case join(title: String, joinHash: String)
}
public let opaqueId: Data public let opaqueId: Data
public let startParam: String? public let target: MessageTarget
public let messageId: MessageId?
public init(opaqueId: Data, startParam: String?, messageId: MessageId?) { public init(opaqueId: Data, target: MessageTarget) {
self.opaqueId = opaqueId self.opaqueId = opaqueId
self.startParam = startParam self.target = target
self.messageId = messageId
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {

View File

@ -11,13 +11,17 @@ public struct ReactionSettings: Equatable, Codable {
} }
} }
func updateReactionSettings(transaction: Transaction, _ f: (ReactionSettings) -> ReactionSettings) {
transaction.updatePreferencesEntry(key: PreferencesKeys.reactionSettings, { current in
let previous = current?.get(ReactionSettings.self) ?? ReactionSettings.default
let updated = f(previous)
return PreferencesEntry(updated)
})
}
public func updateReactionSettingsInteractively(postbox: Postbox, _ f: @escaping (ReactionSettings) -> ReactionSettings) -> Signal<Never, NoError> { public func updateReactionSettingsInteractively(postbox: Postbox, _ f: @escaping (ReactionSettings) -> ReactionSettings) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Void in return postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.reactionSettings, { current in updateReactionSettings(transaction: transaction, f)
let previous = current?.get(ReactionSettings.self) ?? ReactionSettings.default
let updated = f(previous)
return PreferencesEntry(updated)
})
} }
|> ignoreValues |> ignoreValues
} }

View File

@ -6,6 +6,7 @@ import SwiftSignalKit
public final class AvailableReactions: Equatable, Codable { public final class AvailableReactions: Equatable, Codable {
public final class Reaction: Equatable, Codable { public final class Reaction: Equatable, Codable {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case isEnabled
case value case value
case title case title
case staticIcon case staticIcon
@ -15,6 +16,7 @@ public final class AvailableReactions: Equatable, Codable {
case effectAnimation case effectAnimation
} }
public let isEnabled: Bool
public let value: String public let value: String
public let title: String public let title: String
public let staticIcon: TelegramMediaFile public let staticIcon: TelegramMediaFile
@ -24,6 +26,7 @@ public final class AvailableReactions: Equatable, Codable {
public let effectAnimation: TelegramMediaFile public let effectAnimation: TelegramMediaFile
public init( public init(
isEnabled: Bool,
value: String, value: String,
title: String, title: String,
staticIcon: TelegramMediaFile, staticIcon: TelegramMediaFile,
@ -32,6 +35,7 @@ public final class AvailableReactions: Equatable, Codable {
activateAnimation: TelegramMediaFile, activateAnimation: TelegramMediaFile,
effectAnimation: TelegramMediaFile effectAnimation: TelegramMediaFile
) { ) {
self.isEnabled = isEnabled
self.value = value self.value = value
self.title = title self.title = title
self.staticIcon = staticIcon self.staticIcon = staticIcon
@ -42,6 +46,9 @@ public final class AvailableReactions: Equatable, Codable {
} }
public static func ==(lhs: Reaction, rhs: Reaction) -> Bool { public static func ==(lhs: Reaction, rhs: Reaction) -> Bool {
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.value != rhs.value { if lhs.value != rhs.value {
return false return false
} }
@ -69,6 +76,8 @@ public final class AvailableReactions: Equatable, Codable {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.isEnabled = try container.decode(Bool.self, forKey: .isEnabled)
self.value = try container.decode(String.self, forKey: .value) self.value = try container.decode(String.self, forKey: .value)
self.title = try container.decode(String.self, forKey: .title) self.title = try container.decode(String.self, forKey: .title)
@ -91,6 +100,8 @@ public final class AvailableReactions: Equatable, Codable {
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.isEnabled, forKey: .isEnabled)
try container.encode(self.value, forKey: .value) try container.encode(self.value, forKey: .value)
try container.encode(self.title, forKey: .title) try container.encode(self.title, forKey: .title)
@ -146,7 +157,7 @@ public final class AvailableReactions: Equatable, Codable {
private extension AvailableReactions.Reaction { private extension AvailableReactions.Reaction {
convenience init?(apiReaction: Api.AvailableReaction) { convenience init?(apiReaction: Api.AvailableReaction) {
switch apiReaction { switch apiReaction {
case let .availableReaction(reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation): case let .availableReaction(flags, reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation):
guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon) else { guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon) else {
return nil return nil
} }
@ -162,7 +173,9 @@ private extension AvailableReactions.Reaction {
guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation) else { guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation) else {
return nil return nil
} }
let isEnabled = (flags & (1 << 0)) == 0
self.init( self.init(
isEnabled: isEnabled,
value: reaction, value: reaction,
title: title, title: title,
staticIcon: staticIconFile, staticIcon: staticIconFile,
@ -234,6 +247,7 @@ func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) ->
for reaction in availableReactions.reactions { for reaction in availableReactions.reactions {
resources.append(reaction.staticIcon.resource) resources.append(reaction.staticIcon.resource)
resources.append(reaction.appearAnimation.resource)
resources.append(reaction.selectAnimation.resource) resources.append(reaction.selectAnimation.resource)
resources.append(reaction.activateAnimation.resource) resources.append(reaction.activateAnimation.resource)
resources.append(reaction.effectAnimation.resource) resources.append(reaction.effectAnimation.resource)

View File

@ -17,6 +17,14 @@ func updateAppConfigurationOnce(postbox: Postbox, network: Network) -> Signal<Vo
} }
return postbox.transaction { transaction -> Void in return postbox.transaction { transaction -> Void in
if let data = JSON(apiJson: result) { if let data = JSON(apiJson: result) {
if let value = data["reactions_default"] as? String {
updateReactionSettings(transaction: transaction, { settings in
var settings = settings
settings.quickReaction = value
return settings
})
}
updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in
var configuration = configuration var configuration = configuration
configuration.data = data configuration.data = data

View File

@ -499,3 +499,11 @@ func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allo
} }
} }
} }
func _internal_updateDefaultReaction(account: Account, reaction: String) -> Signal<Never, NoError> {
return account.network.request(Api.functions.messages.setDefaultReaction(reaction: reaction))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}

View File

@ -22,19 +22,39 @@ private class AdMessagesHistoryContextImpl {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case peer case peer
case invite
}
struct Invite: Equatable, Codable {
var title: String
var joinHash: String
} }
case peer(PeerId) case peer(PeerId)
case invite(Invite)
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
if let peer = try container.decodeIfPresent(Int64.self, forKey: .peer) { if let peer = try container.decodeIfPresent(Int64.self, forKey: .peer) {
self = .peer(PeerId(peer)) self = .peer(PeerId(peer))
} else if let invite = try container.decodeIfPresent(Invite.self, forKey: .invite) {
self = .invite(invite)
} else { } else {
throw DecodingError.generic throw DecodingError.generic
} }
} }
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .peer(peerId):
try container.encode(peerId.toInt64(), forKey: .peer)
case let .invite(invite):
try container.encode(invite, forKey: .invite)
}
}
} }
public let opaqueId: Data public let opaqueId: Data
@ -133,7 +153,14 @@ private class AdMessagesHistoryContextImpl {
func toMessage(peerId: PeerId, transaction: Transaction) -> Message? { func toMessage(peerId: PeerId, transaction: Transaction) -> Message? {
var attributes: [MessageAttribute] = [] var attributes: [MessageAttribute] = []
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, startParam: self.startParam, messageId: self.messageId)) let target: AdMessageAttribute.MessageTarget
switch self.target {
case let .peer(peerId):
target = .peer(id: peerId, message: self.messageId, startParam: self.startParam)
case let .invite(invite):
target = .join(title: invite.title, joinHash: invite.joinHash)
}
attributes.append(AdMessageAttribute(opaqueId: self.opaqueId, target: target))
if !self.textEntities.isEmpty { if !self.textEntities.isEmpty {
let attribute = TextEntitiesMessageAttribute(entities: self.textEntities) let attribute = TextEntitiesMessageAttribute(entities: self.textEntities)
attributes.append(attribute) attributes.append(attribute)
@ -153,6 +180,23 @@ private class AdMessagesHistoryContextImpl {
} else { } else {
return nil return nil
} }
case let .invite(invite):
author = TelegramChannel(
id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)),
accessHash: nil,
title: invite.title,
username: nil,
photo: [],
creationDate: 0,
version: 0,
participationStatus: .left,
info: .broadcast(TelegramChannelBroadcastInfo(flags: [])),
flags: [],
restrictionInfo: nil,
adminRights: nil,
bannedRights: nil,
defaultBannedRights: nil
)
} }
messagePeers[author.id] = author messagePeers[author.id] = author
@ -368,6 +412,36 @@ private class AdMessagesHistoryContextImpl {
var target: CachedMessage.Target? var target: CachedMessage.Target?
if let fromId = fromId { if let fromId = fromId {
target = .peer(fromId.peerId) target = .peer(fromId.peerId)
} else if let chatInvite = chatInvite, let chatInviteHash = chatInviteHash {
switch chatInvite {
case let .chatInvite(flags, title, _, photo, participantsCount, participants):
let photo = telegramMediaImageFromApiPhoto(photo).flatMap({ smallestImageRepresentation($0.representations) })
let flags: ExternalJoiningChatState.Invite.Flags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0)
let _ = photo
let _ = flags
let _ = participantsCount
let _ = participants
target = .invite(CachedMessage.Target.Invite(
title: title,
joinHash: chatInviteHash
))
case let .chatInvitePeek(chat, _):
if let peer = parseTelegramGroupOrChannel(chat: chat) {
target = .invite(CachedMessage.Target.Invite(
title: peer.debugDisplayTitle,
joinHash: chatInviteHash
))
}
case let .chatInviteAlready(chat):
if let peer = parseTelegramGroupOrChannel(chat: chat) {
target = .invite(CachedMessage.Target.Invite(
title: peer.debugDisplayTitle,
joinHash: chatInviteHash
))
}
}
} }
var messageId: MessageId? var messageId: MessageId?

View File

@ -67,6 +67,7 @@ public enum AdminLogEventAction {
case participantJoinByRequest(invitation: ExportedInvitation, approvedBy: PeerId) case participantJoinByRequest(invitation: ExportedInvitation, approvedBy: PeerId)
case toggleCopyProtection(Bool) case toggleCopyProtection(Bool)
case sendMessage(Message) case sendMessage(Message)
case changeAvailableReactions(previousValue: [String], updatedValue: [String])
} }
public enum ChannelAdminLogEventError { public enum ChannelAdminLogEventError {
@ -262,6 +263,8 @@ func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: PeerId, m
if let message = StoreMessage(apiMessage: message), let rendered = locallyRenderedMessage(message: message, peers: peers) { if let message = StoreMessage(apiMessage: message), let rendered = locallyRenderedMessage(message: message, peers: peers) {
action = .sendMessage(rendered) action = .sendMessage(rendered)
} }
case let .channelAdminLogEventActionChangeAvailableReactions(prevValue, newValue):
action = .changeAvailableReactions(previousValue: prevValue, updatedValue: newValue)
} }
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
if let action = action { if let action = action {

View File

@ -29,21 +29,30 @@ func apiUpdatesGroups(_ updates: Api.Updates) -> [Api.Chat] {
} }
public enum ExternalJoiningChatState { public enum ExternalJoiningChatState {
public struct InviteFlags : Equatable { public struct Invite: Equatable {
public let isChannel: Bool public struct Flags: Equatable, Codable {
public let isBroadcast: Bool public let isChannel: Bool
public let isPublic: Bool public let isBroadcast: Bool
public let isMegagroup: Bool public let isPublic: Bool
public let requestNeeded: Bool public let isMegagroup: Bool
public let requestNeeded: Bool
}
public let flags: Flags
public let title: String
public let about: String?
public let photoRepresentation: TelegramMediaImageRepresentation?
public let participantsCount: Int32
public let participants: [EnginePeer]?
} }
case invite(flags: InviteFlags, title: String, about: String?, photoRepresentation: TelegramMediaImageRepresentation?, participantsCount: Int32, participants: [Peer]?) case invite(Invite)
case alreadyJoined(PeerId) case alreadyJoined(PeerId)
case invalidHash case invalidHash
case peek(PeerId, Int32) case peek(PeerId, Int32)
} }
func _internal_joinChatInteractively(with hash: String, account: Account) -> Signal <PeerId?, JoinLinkError> { func _internal_joinChatInteractively(with hash: String, account: Account) -> Signal<PeerId?, JoinLinkError> {
return account.network.request(Api.functions.messages.importChatInvite(hash: hash), automaticFloodWait: false) return account.network.request(Api.functions.messages.importChatInvite(hash: hash), automaticFloodWait: false)
|> mapError { error -> JoinLinkError in |> mapError { error -> JoinLinkError in
switch error.errorDescription { switch error.errorDescription {
@ -94,8 +103,8 @@ func _internal_joinLinkInformation(_ hash: String, account: Account) -> Signal<E
switch result { switch result {
case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants): case let .chatInvite(flags, title, about, invitePhoto, participantsCount, participants):
let photo = telegramMediaImageFromApiPhoto(invitePhoto).flatMap({ smallestImageRepresentation($0.representations) }) let photo = telegramMediaImageFromApiPhoto(invitePhoto).flatMap({ smallestImageRepresentation($0.representations) })
let flags:ExternalJoiningChatState.InviteFlags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0) let flags: ExternalJoiningChatState.Invite.Flags = .init(isChannel: (flags & (1 << 0)) != 0, isBroadcast: (flags & (1 << 1)) != 0, isPublic: (flags & (1 << 2)) != 0, isMegagroup: (flags & (1 << 3)) != 0, requestNeeded: (flags & (1 << 6)) != 0)
return .single(.invite(flags: flags, title: title, about: about, photoRepresentation: photo, participantsCount: participantsCount, participants: participants?.map({TelegramUser(user: $0)}))) return .single(.invite(ExternalJoiningChatState.Invite(flags: flags, title: title, about: about, photoRepresentation: photo, participantsCount: participantsCount, participants: participants?.map({ EnginePeer(TelegramUser(user: $0)) }))))
case let .chatInviteAlready(chat): case let .chatInviteAlready(chat):
if let peer = parseTelegramGroupOrChannel(chat: chat) { if let peer = parseTelegramGroupOrChannel(chat: chat) {
return account.postbox.transaction({ (transaction) -> ExternalJoiningChatState in return account.postbox.transaction({ (transaction) -> ExternalJoiningChatState in

View File

@ -99,5 +99,14 @@ public extension TelegramEngine {
public func availableReactions() -> Signal<AvailableReactions?, NoError> { public func availableReactions() -> Signal<AvailableReactions?, NoError> {
return _internal_cachedAvailableReactions(postbox: self.account.postbox) return _internal_cachedAvailableReactions(postbox: self.account.postbox)
} }
public func updateQuickReaction(reaction: String) -> Signal<Never, NoError> {
let _ = updateReactionSettingsInteractively(postbox: self.account.postbox, { settings in
var settings = settings
settings.quickReaction = reaction
return settings
}).start()
return _internal_updateDefaultReaction(account: self.account, reaction: reaction)
}
} }
} }

View File

@ -1004,7 +1004,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
actions.context = strongSelf.context actions.context = strongSelf.context
if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions { if canAddMessageReactions(message: topMessage), let availableReactions = availableReactions, let allowedReactions = allowedReactions {
filterReactions: for reaction in availableReactions.reactions { filterReactions: for reaction in availableReactions.reactions {
if !reaction.isEnabled {
continue
}
switch allowedReactions { switch allowedReactions {
case let .set(set): case let .set(set):
if !set.contains(reaction.value) { if !set.contains(reaction.value) {
@ -1015,6 +1019,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
actions.reactionItems.append(ReactionContextItem( actions.reactionItems.append(ReactionContextItem(
reaction: ReactionContextItem.Reaction(rawValue: reaction.value), reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation, stillAnimation: reaction.selectAnimation,
listAnimation: reaction.activateAnimation, listAnimation: reaction.activateAnimation,
applicationAnimation: reaction.effectAnimation applicationAnimation: reaction.effectAnimation
@ -1249,6 +1254,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
theme: strongSelf.presentationData.theme, theme: strongSelf.presentationData.theme,
reaction: ReactionContextItem( reaction: ReactionContextItem(
reaction: ReactionContextItem.Reaction(rawValue: reaction.value), reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation, stillAnimation: reaction.selectAnimation,
listAnimation: reaction.activateAnimation, listAnimation: reaction.activateAnimation,
applicationAnimation: reaction.effectAnimation applicationAnimation: reaction.effectAnimation
@ -3193,6 +3199,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
])]) ])])
strongSelf.chatDisplayNode.dismissInput() strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(actionSheet, in: .window(.root)) strongSelf.present(actionSheet, in: .window(.root))
}, openJoinLink: { [weak self] joinHash in
guard let strongSelf = self else {
return
}
strongSelf.openResolved(result: .join(joinHash), sourceMessageId: nil)
}, requestMessageUpdate: { [weak self] id in }, requestMessageUpdate: { [weak self] id in
if let strongSelf = self { if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)

View File

@ -129,6 +129,7 @@ public final class ChatControllerInteraction {
let updateChoosingSticker: (Bool) -> Void let updateChoosingSticker: (Bool) -> Void
let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void
let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
let openJoinLink: (String) -> Void
let requestMessageUpdate: (MessageId) -> Void let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void let cancelInteractiveKeyboardGestures: () -> Void
@ -227,6 +228,7 @@ public final class ChatControllerInteraction {
updateChoosingSticker: @escaping (Bool) -> Void, updateChoosingSticker: @escaping (Bool) -> Void,
commitEmojiInteraction: @escaping (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void, commitEmojiInteraction: @escaping (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void,
openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void, openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void,
openJoinLink: @escaping (String) -> Void,
requestMessageUpdate: @escaping (MessageId) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void,
automaticMediaDownloadSettings: MediaAutoDownloadSettings, automaticMediaDownloadSettings: MediaAutoDownloadSettings,
@ -311,6 +313,7 @@ public final class ChatControllerInteraction {
self.updateChoosingSticker = updateChoosingSticker self.updateChoosingSticker = updateChoosingSticker
self.commitEmojiInteraction = commitEmojiInteraction self.commitEmojiInteraction = commitEmojiInteraction
self.openLargeEmojiInfo = openLargeEmojiInfo self.openLargeEmojiInfo = openLargeEmojiInfo
self.openJoinLink = openJoinLink
self.requestMessageUpdate = requestMessageUpdate self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
@ -369,6 +372,7 @@ public final class ChatControllerInteraction {
}, updateChoosingSticker: { _ in }, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in }, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in }, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -245,7 +245,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
} else { } else {
if result.last?.1 == ChatMessageWebpageBubbleContentNode.self || if result.last?.1 == ChatMessageWebpageBubbleContentNode.self ||
result.last?.1 == ChatMessagePollBubbleContentNode.self || result.last?.1 == ChatMessagePollBubbleContentNode.self ||
result.last?.1 == ChatMessageContactBubbleContentNode.self { result.last?.1 == ChatMessageContactBubbleContentNode.self ||
result.last?.1 == ChatMessageGameBubbleContentNode.self ||
result.last?.1 == ChatMessageInvoiceBubbleContentNode.self {
result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default))) result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
needReactions = false needReactions = false
} else if result.last?.1 == ChatMessageCommentFooterContentNode.self { } else if result.last?.1 == ChatMessageCommentFooterContentNode.self {
@ -254,11 +256,6 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self { result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self {
result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1) result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1)
} }
/*if result[result.count - 2].1 == ChatMessageTextBubbleContentNode.self {
} else {
result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1)
needReactions = false
}*/
} }
} }
} }

View File

@ -301,8 +301,15 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
isReplyThread = true isReplyThread = true
} }
let trailingWidthToMeasure: CGFloat
if textLayout.hasRTL {
trailingWidthToMeasure = 10000.0
} else {
trailingWidthToMeasure = textLayout.trailingLineWidth
}
let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput
dateLayoutInput = .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message), preferAdditionalInset: false)) dateLayoutInput = .trailingContent(contentWidth: trailingWidthToMeasure, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message), preferAdditionalInset: false))
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
context: item.context, context: item.context,

View File

@ -63,18 +63,23 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
} }
self.contentNode.activateAction = { [weak self] in self.contentNode.activateAction = { [weak self] in
if let strongSelf = self, let item = strongSelf.item { if let strongSelf = self, let item = strongSelf.item {
if let adAttribute = item.message.adAttribute, let author = item.message.author { if let adAttribute = item.message.adAttribute {
let navigationData: ChatControllerInteractionNavigateToPeer switch adAttribute.target {
if let bot = author as? TelegramUser, bot.botInfo != nil, let startParam = adAttribute.startParam { case let .peer(id, messageId, startParam):
navigationData = .withBotStartPayload(ChatControllerInitialBotStart(payload: startParam, behavior: .interactive)) let navigationData: ChatControllerInteractionNavigateToPeer
} else { if let bot = item.message.author as? TelegramUser, bot.botInfo != nil, let startParam = startParam {
var subject: ChatControllerSubject? navigationData = .withBotStartPayload(ChatControllerInitialBotStart(payload: startParam, behavior: .interactive))
if let messageId = adAttribute.messageId { } else {
subject = .message(id: .id(messageId), highlight: true, timecode: nil) var subject: ChatControllerSubject?
if let messageId = messageId {
subject = .message(id: .id(messageId), highlight: true, timecode: nil)
}
navigationData = .chat(textInputState: nil, subject: subject, peekData: nil)
} }
navigationData = .chat(textInputState: nil, subject: subject, peekData: nil) item.controllerInteraction.openPeer(id, navigationData, nil)
case let .join(_, joinHash):
item.controllerInteraction.openJoinLink(joinHash)
} }
item.controllerInteraction.openPeer(author.id, navigationData, nil)
} else { } else {
var webPageContent: TelegramMediaWebpageLoadedContent? var webPageContent: TelegramMediaWebpageLoadedContent?
for media in item.message.media { for media in item.message.media {
@ -342,13 +347,13 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
if let author = item.message.author as? TelegramUser, author.botInfo != nil { if let author = item.message.author as? TelegramUser, author.botInfo != nil {
actionTitle = item.presentationData.strings.Conversation_ViewBot actionTitle = item.presentationData.strings.Conversation_ViewBot
} else if let author = item.message.author as? TelegramChannel, case .group = author.info { } else if let author = item.message.author as? TelegramChannel, case .group = author.info {
if adAttribute.messageId != nil { if case let .peer(_, messageId, _) = adAttribute.target, messageId != nil {
actionTitle = item.presentationData.strings.Conversation_ViewPost actionTitle = item.presentationData.strings.Conversation_ViewPost
} else { } else {
actionTitle = item.presentationData.strings.Conversation_ViewGroup actionTitle = item.presentationData.strings.Conversation_ViewGroup
} }
} else { } else {
if adAttribute.messageId != nil { if case let .peer(_, messageId, _) = adAttribute.target, messageId != nil {
actionTitle = item.presentationData.strings.Conversation_ViewMessage actionTitle = item.presentationData.strings.Conversation_ViewMessage
} else { } else {
actionTitle = item.presentationData.strings.Conversation_ViewChannel actionTitle = item.presentationData.strings.Conversation_ViewChannel

View File

@ -531,6 +531,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, updateChoosingSticker: { _ in }, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in }, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in }, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -1397,6 +1397,38 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let action = TelegramMediaActionType.customText(text: text, entities: entities) let action = TelegramMediaActionType.customText(text: text, entities: entities)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
case let .changeAvailableReactions(_, updatedValue):
var peers = SimpleDictionary<PeerId, Peer>()
var author: Peer?
if let peer = self.entry.peers[self.entry.event.peerId] {
author = peer
peers[peer.id] = peer
}
var text: String = ""
var entities: [MessageTextEntity] = []
let rawText: PresentationStrings.FormattedString
if !updatedValue.isEmpty {
let emojiString = updatedValue.joined(separator: ", ")
rawText = self.presentationData.strings.Channel_AdminLog_AllowedReactionsUpdated(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", emojiString)
} else {
rawText = self.presentationData.strings.Channel_AdminLog_ReactionsDisabled(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "")
}
appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
} else if index == 1 {
return [.Bold]
}
return []
}, to: &text, entities: &entities)
let action = TelegramMediaActionType.customText(text: text, entities: entities)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: []) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
case let .changeTheme(_, updatedValue): case let .changeTheme(_, updatedValue):

View File

@ -157,6 +157,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, updateChoosingSticker: { _ in }, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in }, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in }, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -149,6 +149,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, updateChoosingSticker: { _ in }, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in }, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in }, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil)) }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false), presentationContext: ChatPresentationContext(backgroundNode: nil))

View File

@ -193,7 +193,11 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
self.activateArea.accessibilityValue = item.label.text self.activateArea.accessibilityValue = item.label.text
transition.updateFrame(node: self.labelBadgeNode, frame: labelBadgeNodeFrame) transition.updateFrame(node: self.labelBadgeNode, frame: labelBadgeNodeFrame)
transition.updateFrame(node: self.labelNode, frame: labelFrame) if self.labelNode.bounds.size != labelFrame.size {
self.labelNode.frame = labelFrame
} else {
transition.updateFrame(node: self.labelNode, frame: labelFrame)
}
transition.updateFrame(node: self.textNode, frame: textFrame) transition.updateFrame(node: self.textNode, frame: textFrame)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil) let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)

View File

@ -2241,6 +2241,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, updateChoosingSticker: { _ in }, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in }, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in }, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -1294,6 +1294,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, updateChoosingSticker: { _ in }, updateChoosingSticker: { _ in
}, commitEmojiInteraction: { _, _, _, _ in }, commitEmojiInteraction: { _, _, _, _ in
}, openLargeEmojiInfo: { _, _, _ in }, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, requestMessageUpdate: { _ in }, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,