Assorted fixes

This commit is contained in:
Ali 2020-04-10 13:19:37 +04:00
parent a7282bf2fc
commit 7f02fb759a
32 changed files with 688 additions and 147 deletions

View File

@ -1198,16 +1198,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
text = strongSelf.presentationData.strings.ChatList_TabIconFoldersTooltipEmptyFolders
}
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0), size: CGSize())
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 8.0), size: CGSize())
parentController.present(TooltipScreen(text: text, icon: .chatListPress, location: location, shouldDismissOnTouch: { point in
guard let strongSelf = self, let parentController = strongSelf.parent as? TabBarController else {
return true
return .dismiss(consume: false)
}
if parentController.isPointInsideContentArea(point: point) {
return false
return .ignore
}
return true
return .dismiss(consume: false)
}), in: .current)
}
}

View File

@ -16,6 +16,7 @@ static_library(
"//submodules/AccountContext:AccountContext",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TextFormat:TextFormat",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/AccountContext:AccountContext",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TextFormat:TextFormat",
],
visibility = [
"//visibility:public",

View File

@ -11,6 +11,7 @@ import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import TextFormat
private struct OrderedLinkedListItemOrderingId: RawRepresentable, Hashable {
var rawValue: Int
@ -378,7 +379,7 @@ private enum CreatePollEntry: ItemListNodeEntry {
case let .quizSolutionHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .quizSolutionText(placeholder, text):
return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 400, display: true), sectionId: self.section, style: .blocks, textUpdated: { text in
return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 200, display: true), sectionId: self.section, style: .blocks, textUpdated: { text in
arguments.updateSolutionText(text)
})
case let .quizSolutionInfo(text):
@ -749,6 +750,9 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo
if !hasSelectedOptions {
enabled = false
}
if state.solutionText.count > 200 {
enabled = false
}
}
if nonEmptyOptionCount < 2 {
enabled = false
@ -774,11 +778,14 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo
} else {
publicity = .public
}
var resolvedSolution: String?
var resolvedSolution: TelegramMediaPollResults.Solution?
let kind: TelegramMediaPollKind
if state.isQuiz {
kind = .quiz
resolvedSolution = state.solutionText.isEmpty ? nil : state.solutionText
if !state.solutionText.isEmpty {
let entities = generateTextEntities(state.solutionText, enabledTypes: [.url])
resolvedSolution = TelegramMediaPollResults.Solution(text: state.solutionText, entities: entities)
}
} else {
kind = .poll(multipleAnswers: state.isMultipleChoice)
}

View File

@ -18,4 +18,7 @@ open class GridItemNode: ASDisplayNode {
open func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
}
open func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
}
}

View File

@ -902,6 +902,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
itemNode.frame = item.frame
}
itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads && itemInBounds)
itemNode.updateAbsoluteRect(item.frame, within: bounds.size)
} else {
let itemNode = self.items[item.index].node(layout: presentationLayoutTransition.layout.layout, synchronousLoad: synchronousLoads && itemInBounds)
itemNode.frame = item.frame
@ -909,6 +910,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
addedNodes = true
itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads)
self.setupNode?(itemNode)
itemNode.updateAbsoluteRect(item.frame, within: bounds.size)
}
}

View File

@ -198,6 +198,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
}
}
public final var didScrollWithOffset: ((CGFloat) -> Void)?
private var topItemOverscrollBackground: ListViewOverscrollBackgroundNode?
private var bottomItemOverscrollBackground: ASDisplayNode?
@ -752,6 +754,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
//CATransaction.setDisableActions(true)
let deltaY = scrollView.contentOffset.y - self.lastContentOffset.y
self.didScrollWithOffset?(deltaY)
self.generalAccumulatedDeltaY += deltaY
if abs(self.generalAccumulatedDeltaY) > 14.0 {
let direction: GeneralScrollDirection = self.generalAccumulatedDeltaY < 0 ? .up : .down

View File

@ -287,6 +287,7 @@ public final class ShareController: ViewController {
private let externalShare: Bool
private let immediateExternalShare: Bool
private let subject: ShareControllerSubject
private let presetText: String?
private let switchableAccounts: [AccountWithInfo]
private let immediatePeerId: PeerId?
@ -299,15 +300,16 @@ public final class ShareController: ViewController {
public var dismissed: ((Bool) -> Void)?
public convenience init(context: AccountContext, subject: ShareControllerSubject, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, preferredAction: preferredAction, showInChat: showInChat, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId)
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId)
}
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) {
self.sharedContext = sharedContext
self.currentContext = currentContext
self.currentAccount = currentContext.account
self.subject = subject
self.presetText = presetText
self.externalShare = externalShare
self.immediateExternalShare = immediateExternalShare
self.switchableAccounts = switchableAccounts
@ -428,7 +430,7 @@ public final class ShareController: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = ShareControllerNode(sharedContext: self.sharedContext, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in
self.displayNode = ShareControllerNode(sharedContext: self.sharedContext, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in
self?.requestLayout(transition: transition)
}, presentError: { [weak self] title, text in
guard let strongSelf = self else {
@ -457,9 +459,10 @@ public final class ShareController: ViewController {
for peerId in peerIds {
var messages: [EnqueueMessage] = []
if !text.isEmpty {
messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil))
messages.append(.message(text: url + "\n\n" + text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil))
} else {
messages.append(.message(text: url, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil))
}
messages.append(.message(text: url, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil))
shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages))
}
case let .text(string):

View File

@ -76,7 +76,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private var hapticFeedback: HapticFeedback?
init(sharedContext: SharedAccountContext, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?) {
private let presetText: String?
init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?) {
self.sharedContext = sharedContext
self.presentationData = sharedContext.currentPresentationData.with { $0 }
self.externalShare = externalShare
@ -84,6 +86,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
self.immediatePeerId = immediatePeerId
self.presentError = presentError
self.presetText = presetText
self.defaultAction = defaultAction
self.requestLayout = requestLayout
@ -139,6 +143,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted)
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), placeholder: self.presentationData.strings.ShareMenu_Comment)
self.inputFieldNode.text = presetText ?? ""
self.inputFieldNode.preselectText()
self.inputFieldNode.alpha = 0.0
self.actionSeparatorNode = ASDisplayNode()
@ -176,7 +182,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
strongSelf.peersContentNode?.updateFoundPeers()
}
strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty, inputField: true, actions: strongSelf.defaultAction == nil)
strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil, inputField: true, actions: strongSelf.defaultAction == nil)
strongSelf.updateButton()
@ -228,6 +234,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
}
self.updateButton()
if self.presetText != nil {
self.setActionNodesHidden(false, inputField: true, actions: true, animated: false)
}
}
deinit {
@ -279,13 +289,15 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
self.actionButtonNode.badgeTextColor = presentationData.theme.actionSheet.opaqueItemBackgroundColor
}
func setActionNodesHidden(_ hidden: Bool, inputField: Bool = false, actions: Bool = false) {
func setActionNodesHidden(_ hidden: Bool, inputField: Bool = false, actions: Bool = false, animated: Bool = true) {
func updateActionNodesAlpha(_ nodes: [ASDisplayNode], alpha: CGFloat) {
for node in nodes {
if !node.alpha.isEqual(to: alpha) {
let previousAlpha = node.alpha
node.alpha = alpha
node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: alpha.isZero ? 0.18 : 0.32)
if animated {
node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: alpha.isZero ? 0.18 : 0.32)
}
if let inputNode = node as? ShareInputFieldNode, alpha.isZero {
inputNode.deactivateInput()
@ -362,7 +374,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
if contentNode is ShareSearchContainerNode {
self.setActionNodesHidden(true, inputField: true, actions: true)
} else if !(contentNode is ShareLoadingContainerNode) {
self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty, actions: true)
self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil, actions: true)
}
} else {
if let contentNode = self.contentNode {
@ -409,13 +421,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
var bottomGridInset: CGFloat = 0
var actionButtonHeight: CGFloat = 0
if self.defaultAction != nil || !self.controllerInteraction!.selectedPeers.isEmpty {
if self.defaultAction != nil || !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil {
actionButtonHeight = buttonHeight
bottomGridInset += actionButtonHeight
}
let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, transition: transition)
if !self.controllerInteraction!.selectedPeers.isEmpty {
if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil {
bottomGridInset += inputHeight
}
@ -503,7 +515,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
}
@objc func actionButtonPressed() {
if self.controllerInteraction!.selectedPeers.isEmpty {
if self.controllerInteraction!.selectedPeers.isEmpty && self.presetText == nil {
if let defaultAction = self.defaultAction {
defaultAction.action()
}
@ -658,7 +670,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
let animated = self.peersContentNode == nil
let peersContentNode = SharePeersContainerNode(sharedContext: self.sharedContext, context: context, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, switchToAnotherAccount: { [weak self] in
self?.switchToAnotherAccount?()
})
}, extendedInitialReveal: self.presetText != nil)
self.peersContentNode = peersContentNode
peersContentNode.openSearch = { [weak self] in
let _ = (recentlySearchedPeers(postbox: context.account.postbox)
@ -783,13 +795,21 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate
private func updateButton() {
if self.controllerInteraction!.selectedPeers.isEmpty {
if let defaultAction = self.defaultAction {
if self.presetText != nil {
self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal)
self.actionButtonNode.isEnabled = false
self.actionButtonNode.badge = nil
} else if let defaultAction = self.defaultAction {
self.actionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
self.actionButtonNode.isEnabled = false
self.actionButtonNode.badge = nil
} else {
self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal)
self.actionButtonNode.isEnabled = false
self.actionButtonNode.badge = nil
}
} else {
self.actionButtonNode.isEnabled = true
self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
self.actionButtonNode.badge = "\(self.controllerInteraction!.selectedPeers.count)"
}

View File

@ -68,6 +68,8 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
private let inputInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 10.0, right: 16.0)
private let accessoryButtonsWidth: CGFloat = 10.0
private var selectTextOnce: Bool = false
var text: String {
get {
return self.textInputNode.attributedText?.string ?? ""
@ -124,9 +126,15 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
self.addSubnode(self.placeholderNode)
self.addSubnode(self.clearButton)
self.textInputNode.textView.showsVerticalScrollIndicator = false
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
}
func preselectText() {
self.selectTextOnce = true
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
@ -165,6 +173,13 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.placeholderNode.isHidden = true
self.clearButton.isHidden = false
if self.selectTextOnce {
self.selectTextOnce = false
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
self.textInputNode.selectedRange = NSRange(self.text.startIndex ..< self.text.endIndex, in: self.text)
})
}
}
func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
@ -196,5 +211,6 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
@objc func clearPressed() {
self.textInputNode.attributedText = nil
self.deactivateInput()
self.updateHeight?()
}
}

View File

@ -80,6 +80,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
private let nameDisplayOrder: PresentationPersonNameOrder
private let controllerInteraction: ShareControllerInteraction
private let switchToAnotherAccount: () -> Void
private let extendedInitialReveal: Bool
let accountPeer: Peer
private let foundPeers = Promise<[RenderedPeer]>([])
@ -107,7 +108,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
let peersValue = Promise<[(RenderedPeer, PeerPresence?)]>()
init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void) {
init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, extendedInitialReveal: Bool) {
self.sharedContext = sharedContext
self.context = context
self.theme = theme
@ -116,6 +117,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
self.controllerInteraction = controllerInteraction
self.accountPeer = accountPeer
self.switchToAnotherAccount = switchToAnotherAccount
self.extendedInitialReveal = extendedInitialReveal
self.peersValue.set(.single(peers))
@ -261,7 +263,12 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
var rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0)
rowCount = max(rowCount, 4)
let minimallyRevealedRowCount: CGFloat = 3.7
let minimallyRevealedRowCount: CGFloat
if self.extendedInitialReveal {
minimallyRevealedRowCount = 4.6
} else {
minimallyRevealedRowCount = 3.7
}
let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount))
let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0)

View File

@ -0,0 +1,16 @@
load("//Config:buck_rule_macros.bzl", "static_library")
static_library(
name = "ShimmerEffect",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
],
)

View File

@ -0,0 +1,16 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ShimmerEffect",
module_name = "ShimmerEffect",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,200 @@
import UIKit
import AsyncDisplayKit
import Display
private final class ShimmerEffectForegroundNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
super.init()
self.isLayerBacked = true
self.clipsToBounds = true
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
} else {
self.updateAnimation()
}
}
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
}
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
self.shouldBeAnimating = shouldBeAnimating
if shouldBeAnimating {
self.addImageAnimation()
} else {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
}
let gradientHeight: CGFloat = 250.0
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
animation.beginTime = 1.0
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
public final class ShimmerEffectNode: ASDisplayNode {
public enum Shape: Equatable {
case circle(CGRect)
case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat)
case roundedRect(rect: CGRect, cornerRadius: CGFloat)
}
private let backgroundNode: ASDisplayNode
private let effectNode: ShimmerEffectForegroundNode
private let foregroundNode: ASImageNode
private var currentShapes: [Shape] = []
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private var currentShimmeringColor: UIColor?
private var currentSize = CGSize()
override public init() {
self.backgroundNode = ASDisplayNode()
self.effectNode = ShimmerEffectForegroundNode()
self.foregroundNode = ASImageNode()
self.foregroundNode.displaysAsynchronously = false
self.foregroundNode.displayWithoutProcessing = true
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.effectNode)
self.addSubnode(self.foregroundNode)
}
public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.effectNode.updateAbsoluteRect(rect, within: containerSize)
}
public func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], size: CGSize) {
if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.currentShimmeringColor = shimmeringColor
self.currentShapes = shapes
self.currentSize = size
self.backgroundNode.backgroundColor = foregroundColor
self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor)
self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in
context.setFillColor(backgroundColor.cgColor)
context.setBlendMode(.copy)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.clear.cgColor)
for shape in shapes {
switch shape {
case let .circle(frame):
context.fillEllipse(in: frame)
case let .roundedRectLine(startPoint, width, diameter):
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
case let .roundedRect(rect, radius):
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: radius, height: radius))
UIGraphicsPushContext(context)
path.fill()
UIGraphicsPopContext()
}
}
})
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.effectNode.frame = CGRect(origin: CGPoint(), size: size)
}
}

View File

@ -25,6 +25,7 @@ static_library(
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice",
"//submodules/ShimmerEffect:ShimmerEffect",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -26,6 +26,7 @@ swift_library(
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice",
"//submodules/ShimmerEffect:ShimmerEffect",
],
visibility = [
"//visibility:public",

View File

@ -11,6 +11,7 @@ import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import TelegramPresentationData
import ShimmerEffect
final class StickerPackPreviewInteraction {
var previewedItem: StickerPreviewPeekItem?
@ -60,7 +61,9 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
private var isEmpty: Bool?
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private let placeholderNode: ASDisplayNode
private var placeholderNode: ShimmerEffectNode?
private var theme: PresentationTheme?
override var isVisibleInGrid: Bool {
didSet {
@ -83,23 +86,22 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
override init() {
self.imageNode = TransformImageNode()
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
self.placeholderNode = ASDisplayNode()
self.placeholderNode = ShimmerEffectNode()
super.init()
self.addSubnode(self.imageNode)
self.addSubnode(self.placeholderNode)
if let placeholderNode = self.placeholderNode {
self.addSubnode(placeholderNode)
}
var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil, !strongSelf.placeholderNode.alpha.isZero {
strongSelf.placeholderNode.alpha = 0.0
if !firstTime {
strongSelf.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
}
firstTime = false
}
@ -109,6 +111,20 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
self.stickerFetchedDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
if !animated {
placeholderNode.removeFromSupernode()
} else {
placeholderNode.alpha = 0.0
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
placeholderNode?.removeFromSupernode()
})
}
}
}
override func didLoad() {
super.didLoad()
@ -117,8 +133,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
func setup(account: Account, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) {
self.interaction = interaction
self.placeholderNode.backgroundColor = theme.list.mediaPlaceholderColor
self.theme = theme
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem || self.isEmpty != isEmpty {
if let stickerItem = stickerItem {
@ -133,7 +148,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
self.addSubnode(animationNode)
animationNode.started = { [weak self] in
self?.imageNode.isHidden = true
self?.placeholderNode.alpha = 0.0
self?.removePlaceholder(animated: false)
}
}
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
@ -151,13 +166,15 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
}
}
} else {
if isEmpty {
if !self.placeholderNode.alpha.isZero {
self.placeholderNode.alpha = 0.0
self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
if let placeholderNode = self.placeholderNode {
if isEmpty {
if !placeholderNode.alpha.isZero {
placeholderNode.alpha = 0.0
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
}
} else {
placeholderNode.alpha = 1.0
}
} else {
self.placeholderNode.alpha = 1.0
}
}
self.currentState = (account, stickerItem)
@ -176,7 +193,14 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0)
let boundingSize = CGSize(width: boundsSide, height: boundsSide)
self.placeholderNode.frame = CGRect(origin: CGPoint(x: floor((bounds.width - boundingSize.width) / 2.0), y: floor((bounds.height - boundingSize.height) / 2.0)), size: boundingSize)
if let placeholderNode = self.placeholderNode {
let placeholderFrame = CGRect(origin: CGPoint(x: floor((bounds.width - boundingSize.width) / 2.0), y: floor((bounds.height - boundingSize.height) / 2.0)), size: boundingSize)
placeholderNode.frame = bounds
if let theme = self.theme {
placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.roundedRect(rect: placeholderFrame, cornerRadius: 10.0)], size: bounds.size)
}
}
if let (_, item) = self.currentState {
if let item = item, let dimensions = item.file.dimensions?.cgSize {
@ -191,6 +215,12 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
}
}
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
if let placeholderNode = self.placeholderNode {
placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize)
}
}
func transitionNode() -> ASDisplayNode? {
return self
}

View File

@ -10,6 +10,7 @@ import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import ShimmerEffect
private struct StickerPackPreviewGridEntry: Comparable, Identifiable {
let index: Int
@ -51,6 +52,7 @@ private enum StickerPackAction {
private enum StickerPackNextAction {
case navigatedNext
case dismiss
case ignored
}
private final class StickerPackContainer: ASDisplayNode {
@ -68,7 +70,7 @@ private final class StickerPackContainer: ASDisplayNode {
private let actionAreaSeparatorNode: ASDisplayNode
private let buttonNode: HighlightableButtonNode
private let titleNode: ImmediateTextNode
private let titlePlaceholderNode: ASDisplayNode
private var titlePlaceholderNode: ShimmerEffectNode?
private let titleContainer: ASDisplayNode
private let titleSeparatorNode: ASDisplayNode
@ -80,6 +82,7 @@ private final class StickerPackContainer: ASDisplayNode {
private var itemsDisposable: Disposable?
private(set) var currentStickerPack: (StickerPackCollectionInfo, [ItemCollectionItem], Bool)?
private var didReceiveStickerPackResult = false
private let isReadyValue = Promise<Bool>()
private var didSetReady = false
@ -121,16 +124,13 @@ private final class StickerPackContainer: ASDisplayNode {
self.actionAreaBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
self.actionAreaSeparatorNode = ASDisplayNode()
self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.buttonNode = HighlightableButtonNode()
self.titleNode = ImmediateTextNode()
self.titlePlaceholderNode = ASDisplayNode()
self.titlePlaceholderNode.alpha = 0.0
self.titlePlaceholderNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor
self.titleContainer = ASDisplayNode()
self.titleSeparatorNode = ASDisplayNode()
self.titleSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true)
@ -143,7 +143,6 @@ private final class StickerPackContainer: ASDisplayNode {
self.addSubnode(self.buttonNode)
self.titleContainer.addSubnode(self.titleNode)
self.titleContainer.addSubnode(self.titlePlaceholderNode)
self.addSubnode(self.titleContainer)
self.addSubnode(self.titleSeparatorNode)
@ -354,13 +353,15 @@ private final class StickerPackContainer: ASDisplayNode {
switch strongSelf.decideNextAction(strongSelf, installed ? .remove : .add) {
case .dismiss:
strongSelf.requestDismiss()
case .navigatedNext:
case .navigatedNext, .ignored:
strongSelf.updateStickerPackContents(.result(info: info, items: items, installed: !installed))
}
})
}
private func updateStickerPackContents(_ contents: LoadedStickerPack) {
self.didReceiveStickerPackResult = true
var entries: [StickerPackPreviewGridEntry] = []
var updateLayout = false
@ -393,7 +394,11 @@ private final class StickerPackContainer: ASDisplayNode {
self.nextStableId += 1
entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: false))
}
self.titlePlaceholderNode.alpha = 1.0
if self.titlePlaceholderNode == nil {
let titlePlaceholderNode = ShimmerEffectNode()
self.titlePlaceholderNode = titlePlaceholderNode
self.titleContainer.addSubnode(titlePlaceholderNode)
}
case .none:
entries = []
self.buttonNode.setTitle(self.presentationData.strings.Common_Close.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal)
@ -404,10 +409,9 @@ private final class StickerPackContainer: ASDisplayNode {
self.nextStableId += 1
entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: true))
}
if !self.titlePlaceholderNode.alpha.isZero {
self.titlePlaceholderNode.alpha = 0.0
self.titlePlaceholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
if let titlePlaceholderNode = self.titlePlaceholderNode {
self.titlePlaceholderNode = nil
titlePlaceholderNode.removeFromSupernode()
}
case let .result(info, items, installed):
if !items.isEmpty && self.currentStickerPack == nil {
@ -441,15 +445,12 @@ private final class StickerPackContainer: ASDisplayNode {
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
self.buttonNode.setBackgroundImage(roundedAccentBackground, for: [])
if self.titleNode.attributedText == nil {
self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
if self.titleNode.attributedText == nil {
if !self.titlePlaceholderNode.alpha.isZero {
self.titlePlaceholderNode.alpha = 0.0
self.titlePlaceholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
if let titlePlaceholderNode = self.titlePlaceholderNode {
self.titlePlaceholderNode = nil
titlePlaceholderNode.removeFromSupernode()
}
}
@ -480,6 +481,15 @@ private final class StickerPackContainer: ASDisplayNode {
let previousEntries = self.currentEntries
self.currentEntries = entries
if let titlePlaceholderNode = self.titlePlaceholderNode {
let fakeTitleSize = CGSize(width: 160.0, height: 22.0)
let titlePlaceholderFrame = CGRect(origin: CGPoint(x: floor((-fakeTitleSize.width) / 2.0), y: floor((-fakeTitleSize.height) / 2.0)), size: fakeTitleSize)
titlePlaceholderNode.frame = titlePlaceholderFrame
let theme = self.presentationData.theme
titlePlaceholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.roundedRect(rect: CGRect(origin: CGPoint(), size: titlePlaceholderFrame.size), cornerRadius: 4.0)], size: titlePlaceholderFrame.size)
updateLayout = true
}
if updateLayout, let (layout, _, _, _) = self.validLayout {
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 12.0 * 2.0, height: .greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize)
@ -487,10 +497,6 @@ private final class StickerPackContainer: ASDisplayNode {
self.updateLayout(layout: layout, transition: .immediate)
}
let fakeTitleSize = CGSize(width: 160.0, height: 22.0)
self.titlePlaceholderNode.frame = CGRect(origin: CGPoint(x: floor((-fakeTitleSize.width) / 2.0), y: floor((-fakeTitleSize.height) / 2.0)), size: fakeTitleSize)
let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme, scrollToItem: scrollToItem)
self.enqueueTransaction(transaction)
}
@ -571,12 +577,18 @@ private final class StickerPackContainer: ASDisplayNode {
guard let strongSelf = self else {
return
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf.isReadyValue.set(.single(true))
if strongSelf.didReceiveStickerPackResult {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf.isReadyValue.set(.single(true))
}
}
})
if let titlePlaceholderNode = self.titlePlaceholderNode {
titlePlaceholderNode.updateAbsoluteRect(titlePlaceholderNode.frame.offsetBy(dx: self.titleContainer.frame.minX, dy: self.titleContainer.frame.minY - gridInsets.top - gridFrame.minY), within: gridFrame.size)
}
if firstTime {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
@ -596,6 +608,7 @@ private final class StickerPackContainer: ASDisplayNode {
let expandHeight: CGFloat = 100.0
let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight))
let expandScrollProgress = 1.0 - max(0.0, min(1.0, presentationLayout.contentOffset.y / (-gridInsets.top)))
let modalProgress = max(0.0, min(1.0, expandScrollProgress))
let expandProgressTransition = transition
var expandUpdated = false
@ -610,6 +623,11 @@ private final class StickerPackContainer: ASDisplayNode {
expandUpdated = true
}
if abs(self.modalProgress - modalProgress) > CGFloat.ulpOfOne {
self.modalProgress = modalProgress
expandUpdated = true
}
if expandUpdated {
self.expandProgressUpdated(self, expandProgressTransition, self.isAnimatingAutoscroll ? transition : .immediate)
}
@ -703,7 +721,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
self.sendSticker = sendSticker
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
self.dimNode.alpha = 0.0
self.containerContainingNode = ASDisplayNode()
@ -769,7 +787,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
switch action {
case .add:
var allAdded = true
for i in index + 1 ..< strongSelf.stickerPacks.count {
for _ in index + 1 ..< strongSelf.stickerPacks.count {
if let container = strongSelf.containers[index], let (_, _, installed) = container.currentStickerPack {
if !installed {
allAdded = false
@ -784,6 +802,8 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
case .remove:
if strongSelf.stickerPacks.count == 1 {
return .dismiss
} else {
return .ignored
}
}
}
@ -801,7 +821,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
let modalProgress = container.modalProgress
strongSelf.modalProgressUpdated(modalProgress, transition)
strongSelf.containerLayoutUpdated(layout, transition: expandTransition)
for (otherIndex, otherContainer) in strongSelf.containers {
for (_, otherContainer) in strongSelf.containers {
if otherContainer !== container {
otherContainer.syncExpandProgress(expandScrollProgress: container.expandScrollProgress, expandProgress: container.expandProgress, modalProgress: container.modalProgress, transition: expandTransition)
}
@ -881,11 +901,16 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
let deltaOffset = self.relativeToSelectedStickerPackTransition
self.relativeToSelectedStickerPackTransition = 0.0
if let layout = self.validLayout {
let existingIndices = Array(self.containers.keys)
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
self.containerLayoutUpdated(layout, transition: transition)
var previousFrames: [Int: CGRect] = [:]
for (key, container) in self.containers {
if !existingIndices.contains(key) {
previousFrames[key] = container.frame
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
self.containerLayoutUpdated(layout, transition: .immediate)
for (key, container) in self.containers {
if let previousFrame = previousFrames[key] {
transition.animatePositionAdditive(node: container, offset: CGPoint(x: previousFrame.minX - container.frame.minX, y: 0.0))
} else {
transition.animatePositionAdditive(node: container, offset: CGPoint(x: -deltaOffset, y: 0.0))
}
}
@ -936,8 +961,18 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
}
}
let result = super.hitTest(point, with: event)
return result
if let result = super.hitTest(point, with: event) {
for (index, container) in self.containers {
if result.isDescendant(of: container.view) {
if index != self.selectedStickerPackIndex {
return self.containerContainingNode.view
}
}
}
return result
} else {
return nil
}
}
@objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) {

View File

@ -50,12 +50,22 @@ public struct TelegramMediaPollOptionVoters: Equatable, PostboxCoding {
}
public struct TelegramMediaPollResults: Equatable, PostboxCoding {
public struct Solution: Equatable {
public let text: String
public let entities: [MessageTextEntity]
public init(text: String, entities: [MessageTextEntity]) {
self.text = text
self.entities = entities
}
}
public let voters: [TelegramMediaPollOptionVoters]?
public let totalVoters: Int32?
public let recentVoters: [PeerId]
public let solution: String?
public let solution: TelegramMediaPollResults.Solution?
public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId], solution: String?) {
public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId], solution: TelegramMediaPollResults.Solution?) {
self.voters = voters
self.totalVoters = totalVoters
self.recentVoters = recentVoters
@ -66,7 +76,12 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding {
self.voters = decoder.decodeOptionalObjectArrayWithDecoderForKey("v")
self.totalVoters = decoder.decodeOptionalInt32ForKey("t")
self.recentVoters = decoder.decodeInt64ArrayForKey("rv").map(PeerId.init)
self.solution = decoder.decodeOptionalStringForKey("sol")
if let text = decoder.decodeOptionalStringForKey("sol") {
let entities: [MessageTextEntity] = decoder.decodeObjectArrayWithDecoderForKey("solent")
self.solution = TelegramMediaPollResults.Solution(text: text, entities: entities)
} else {
self.solution = nil
}
}
public func encode(_ encoder: PostboxEncoder) {
@ -82,7 +97,8 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding {
}
encoder.encodeInt64Array(self.recentVoters.map { $0.toInt64() }, forKey: "rv")
if let solution = self.solution {
encoder.encodeString(solution, forKey: "sol")
encoder.encodeString(solution.text, forKey: "sol")
encoder.encodeObjectArray(solution.entities, forKey: "solent")
} else {
encoder.encodeNil(forKey: "sol")
}
@ -248,11 +264,11 @@ public final class TelegramMediaPoll: Media, Equatable {
}
updatedResults = TelegramMediaPollResults(voters: updatedVoters.map({ voters in
return TelegramMediaPollOptionVoters(selected: selectedOpaqueIdentifiers.contains(voters.opaqueIdentifier), opaqueIdentifier: voters.opaqueIdentifier, count: voters.count, isCorrect: correctOpaqueIdentifiers.contains(voters.opaqueIdentifier))
}), totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution)
}), totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution)
} else if let updatedVoters = results.voters {
updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution)
updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution)
} else {
updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution)
updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution)
}
} else {
updatedResults = results

View File

@ -14,7 +14,7 @@ public enum CachedStickerPackResult {
func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo, items: [ItemCollectionItem], reference: StickerPackReference? = nil) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(info.id)), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: info.shortName)), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: info.shortName.lowercased())), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec)
if let reference = reference {
var namespace: Int32?
@ -62,7 +62,7 @@ public func cachedStickerPack(postbox: Postbox, network: Network, reference: Sti
return (.fetching, true, nil)
}
case let .name(shortName):
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName))) as? CachedStickerPack, let info = cached.info {
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName.lowercased()))) as? CachedStickerPack, let info = cached.info {
previousHash = cached.hash
let current: CachedStickerPackResult = .result(info, cached.items, false)
if cached.hash != info.hash {
@ -158,7 +158,7 @@ func cachedStickerPack(transaction: Transaction, reference: StickerPackReference
}
}
}
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName))) as? CachedStickerPack, let info = cached.info {
if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName.lowercased()))) as? CachedStickerPack, let info = cached.info {
return (info, cached.items, false)
}
case .animatedEmoji:

View File

@ -178,10 +178,14 @@ func mediaContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods:
if poll.deadlineTimeout != nil {
pollFlags |= 1 << 4
}
if poll.results.solution != nil {
var mappedSolution: String?
var mappedSolutionEntities: [Api.MessageEntity]?
if let solution = poll.results.solution {
mappedSolution = solution.text
mappedSolutionEntities = apiTextAttributeEntities(TextEntitiesMessageAttribute(entities: solution.entities), associatedPeers: SimpleDictionary())
pollMediaFlags |= 1 << 1
}
let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: poll.results.solution, solutionEntities: poll.results.solution != nil ? [] : nil)
let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities)
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil)))
} else if let _ = media as? TelegramMediaDice {
let input = Api.InputMedia.inputMediaDice

View File

@ -129,11 +129,16 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A
if poll.deadlineTimeout != nil {
pollFlags |= 1 << 4
}
if poll.results.solution != nil {
var mappedSolution: String?
var mappedSolutionEntities: [Api.MessageEntity]?
if let solution = poll.results.solution {
mappedSolution = solution.text
mappedSolutionEntities = apiTextAttributeEntities(TextEntitiesMessageAttribute(entities: solution.entities), associatedPeers: SimpleDictionary())
pollMediaFlags |= 1 << 1
}
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: poll.results.solution, solutionEntities: poll.results.solution != nil ? [] : nil), replyMarkup: nil, entities: nil, scheduleDate: nil))
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities), replyMarkup: nil, entities: nil, scheduleDate: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)

View File

@ -29,10 +29,15 @@ extension TelegramMediaPollOptionVoters {
extension TelegramMediaPollResults {
init(apiResults: Api.PollResults) {
switch apiResults {
case let .pollResults(_, results, totalVoters, recentVoters, solution, _):
case let .pollResults(_, results, totalVoters, recentVoters, solution, solutionEntities):
var parsedSolution: TelegramMediaPollResults.Solution?
if let solution = solution, let solutionEntities = solutionEntities, !solution.isEmpty {
parsedSolution = TelegramMediaPollResults.Solution(text: solution, entities: messageTextEntitiesFromApiEntities(solutionEntities))
}
self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters, recentVoters: recentVoters.flatMap { recentVoters in
return recentVoters.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: $0) }
} ?? [], solution: solution)
} ?? [], solution: parsedSolution)
}
}
}

View File

@ -286,6 +286,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private weak var mediaRestrictedTooltipController: TooltipController?
private var mediaRestrictedTooltipControllerMode = true
private var currentMessageTooltipScreens: [TooltipScreen] = []
private weak var slowmodeTooltipController: ChatSlowmodeHintController?
private weak var currentContextController: ContextController?
@ -1695,11 +1697,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let solution = resultPoll.results.solution {
for contentNode in itemNode.contentNodes {
if let contentNode = contentNode as? ChatMessagePollBubbleContentNode, let sourceNode = contentNode.solutionTipSourceNode {
let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view)
strongSelf.present(TooltipScreen(text: solution, icon: .info, location: absoluteFrame, shouldDismissOnTouch: { _ in
return true
}), in: .current)
let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0)
let tooltipScreen = TooltipScreen(text: solution.text, textEntities: solution.entities, icon: nil, location: absoluteFrame, shouldDismissOnTouch: { point in
return .dismiss(consume: absoluteFrame.contains(point))
}, openUrl: { url in
self?.openUrl(url, concealed: false)
})
tooltipScreen.becameDismissed = { tooltipScreen in
guard let strongSelf = self else {
return
}
strongSelf.currentMessageTooltipScreens.removeAll(where: { $0 === tooltipScreen })
}
strongSelf.currentMessageTooltipScreens.append(tooltipScreen)
strongSelf.present(tooltipScreen, in: .current)
}
}
}
@ -1947,14 +1958,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
strongSelf.presentPollCreation(isQuiz: isQuiz)
}, displayPollSolution: { [weak self] text, sourceNode in
}, displayPollSolution: { [weak self] solution, sourceNode in
guard let strongSelf = self else {
return
}
let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -12.0, dy: 0.0)
strongSelf.present(TooltipScreen(text: text, icon: .info, location: absoluteFrame, shouldDismissOnTouch: { _ in
return true
}), in: .current)
let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0)
let tooltipScreen = TooltipScreen(text: solution.text, textEntities: solution.entities, icon: nil, location: absoluteFrame, shouldDismissOnTouch: { point in
return .dismiss(consume: absoluteFrame.contains(point))
}, openUrl: { url in
self?.openUrl(url, concealed: false)
})
tooltipScreen.becameDismissed = { tooltipScreen in
guard let strongSelf = self else {
return
}
strongSelf.currentMessageTooltipScreens.removeAll(where: { $0 === tooltipScreen })
}
strongSelf.currentMessageTooltipScreens.append(tooltipScreen)
strongSelf.present(tooltipScreen, in: .current)
}, requestMessageUpdate: { [weak self] id in
if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
@ -2668,6 +2689,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
override public func loadDisplayNode() {
self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, controller: self)
self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset in
guard let strongSelf = self else {
return
}
for tooltipScreen in strongSelf.currentMessageTooltipScreens {
tooltipScreen.addRelativeScrollingOffset(-offset)
}
}
self.chatDisplayNode.peerView = self.peerView
let initialData = self.chatDisplayNode.historyNode.initialData

View File

@ -107,7 +107,7 @@ public final class ChatControllerInteraction {
let dismissReplyMarkupMessage: (Message) -> Void
let openMessagePollResults: (MessageId, Data) -> Void
let openPollCreation: (Bool?) -> Void
let displayPollSolution: (String, ASDisplayNode) -> Void
let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void
let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void
@ -122,7 +122,7 @@ public final class ChatControllerInteraction {
var searchTextHighightState: (String, [MessageIndex])?
var seenOneTimeAnimatedMedia = Set<MessageId>()
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (String, ASDisplayNode) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
self.openMessage = openMessage
self.openPeer = openPeer
self.openPeerMention = openPeerMention

View File

@ -996,8 +996,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
let additionalTextRightInset: CGFloat = 24.0
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height)
let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset - additionalTextRightInset, height: constrainedSize.height)
var edited = false
if item.attributes.updatingMedia != nil {
@ -1142,6 +1144,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
var boundingSize: CGSize = textFrameWithoutInsets.size
boundingSize.width += additionalTextRightInset
boundingSize.width = max(boundingSize.width, typeLayout.size.width)
boundingSize.width = max(boundingSize.width, votersLayout.size.width + 4.0 + (statusSize?.width ?? 0.0))
boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0 + (statusSize?.width ?? 0.0))
@ -1456,7 +1459,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
}
if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .quiz = poll.kind, let solution = poll.results.solution, !solution.isEmpty, (isClosed || hasSelected) {
if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .quiz = poll.kind, let solution = poll.results.solution, (isClosed || hasSelected) {
let solutionButtonNode: SolutionButtonNode
if let current = strongSelf.solutionButtonNode {
solutionButtonNode = current

View File

@ -19,6 +19,7 @@ import JoinLinkPreviewUI
import LanguageLinkPreviewUI
import SettingsUI
import UrlHandling
import ShareController
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
if case .default = navigation {
@ -224,25 +225,46 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
}
if let to = to {
let query = to.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789").inverted)
let _ = (context.account.postbox.searchContacts(query: query)
|> deliverOnMainQueue).start(next: { (peers, _) in
for case let peer as TelegramUser in peers {
if peer.phone == query {
continueWithPeer(peer.id)
break
if to.hasPrefix("@") {
let _ = (resolvePeerByName(account: context.account, name: String(to[to.index(to.startIndex, offsetBy: 1)...]))
|> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId {
let _ = (context.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { peer in
context.sharedContext.applicationBindings.dismissNativeController()
continueWithPeer(peer.id)
})
}
})
} else {
let query = to.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789").inverted)
let _ = (context.account.postbox.searchContacts(query: query)
|> deliverOnMainQueue).start(next: { (peers, _) in
for case let peer as TelegramUser in peers {
if peer.phone == query {
context.sharedContext.applicationBindings.dismissNativeController()
continueWithPeer(peer.id)
break
}
}
})
}
} else {
if let url = url, !url.isEmpty {
let shareController = ShareController(context: context, subject: .url(url), presetText: text, externalShare: false, immediateExternalShare: false)
present(shareController, nil)
context.sharedContext.applicationBindings.dismissNativeController()
} else {
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
controller.peerSelected = { [weak controller] peerId in
if let strongController = controller {
strongController.dismiss()
continueWithPeer(peerId)
}
}
})
} else {
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled]))
controller.peerSelected = { [weak controller] peerId in
if let strongController = controller {
strongController.dismiss()
continueWithPeer(peerId)
}
context.sharedContext.applicationBindings.dismissNativeController()
navigationController?.pushViewController(controller)
}
navigationController?.pushViewController(controller)
}
case let .wallpaper(parameter):
let presentationData = context.sharedContext.currentPresentationData.with { $0 }

View File

@ -401,10 +401,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
var availablePanes = availablePanes
if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData {
if cachedData.commonGroupCount != 0 {
if ignoreGroupInCommon != nil && cachedData.commonGroupCount == 1 {
} else {
availablePanes?.append(.groupsInCommon)
}
availablePanes?.append(.groupsInCommon)
}
}

View File

@ -11,9 +11,8 @@ private func textForTimeout(value: Int) -> String {
} else {
let minutes = value / 60
let seconds = value % 60
let minutesPadding = minutes < 10 ? "0" : ""
let secondsPadding = seconds < 10 ? "0" : ""
return "\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)"
return "\(minutes):\(secondsPadding)\(seconds)"
}
}

View File

@ -11,6 +11,9 @@ static_library(
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AppBundle:AppBundle",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/SyncCore:SyncCore",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TextFormat:TextFormat",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -12,6 +12,9 @@ swift_library(
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AppBundle:AppBundle",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/SyncCore:SyncCore",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TextFormat:TextFormat",
],
visibility = [
"//visibility:public",

View File

@ -5,13 +5,17 @@ import Display
import TelegramPresentationData
import AnimatedStickerNode
import AppBundle
import SyncCore
import TelegramCore
import TextFormat
private final class TooltipScreenNode: ViewControllerTracingNode {
private let icon: TooltipScreen.Icon
private let icon: TooltipScreen.Icon?
private let location: CGRect
private let shouldDismissOnTouch: (CGPoint) -> Bool
private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch
private let requestDismiss: () -> Void
private let scrollingContainer: ASDisplayNode
private let containerNode: ASDisplayNode
private let backgroundNode: ASImageNode
private let arrowNode: ASImageNode
@ -19,7 +23,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
private let animatedStickerNode: AnimatedStickerNode
private let textNode: ImmediateTextNode
init(text: String, icon: TooltipScreen.Icon, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> Bool, requestDismiss: @escaping () -> Void) {
private var isArrowInverted: Bool = false
init(text: String, textEntities: [MessageTextEntity], icon: TooltipScreen.Icon?, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openUrl: @escaping (String) -> Void) {
self.icon = icon
self.location = location
self.shouldDismissOnTouch = shouldDismissOnTouch
@ -29,6 +35,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
let fillColor = UIColor(white: 0.0, alpha: 0.8)
self.scrollingContainer = ASDisplayNode()
self.backgroundNode = ASImageNode()
self.backgroundNode.image = generateAdjustedStretchableFilledCircleImage(diameter: 15.0, color: fillColor)
@ -47,10 +55,13 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
self.textNode.attributedText = stringWithAppliedEntities(text, entities: textEntities, baseColor: .white, linkColor: .white, baseFont: Font.regular(14.0), linkFont: Font.regular(14.0), boldFont: Font.semibold(14.0), italicFont: Font.italic(14.0), boldItalicFont: Font.semiboldItalic(14.0), fixedFont: Font.monospace(14.0), blockQuoteFont: Font.regular(14.0), underlineLinks: true, external: false)
self.animatedStickerNode = AnimatedStickerNode()
switch icon {
case .none:
break
case .chatListPress:
if let path = getAppBundle().path(forResource: "ChatListFoldersTooltip", ofType: "json") {
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct)
@ -70,36 +81,80 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.textNode)
self.containerNode.addSubnode(self.animatedStickerNode)
self.addSubnode(self.containerNode)
self.scrollingContainer.addSubnode(self.containerNode)
self.addSubnode(self.scrollingContainer)
self.textNode.linkHighlightColor = UIColor.white.withAlphaComponent(0.5)
self.textNode.highlightAttributeAction = { attributes in
let highlightedAttributes = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag,
TelegramTextAttributes.Timecode
]
for attribute in highlightedAttributes {
if let _ = attributes[NSAttributedString.Key(rawValue: attribute)] {
return NSAttributedString.Key(rawValue: attribute)
}
}
return nil
}
self.textNode.tapAttributeAction = { [weak self] attributes in
guard let strongSelf = self else {
return
}
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
openUrl(url)
}
}
}
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.scrollingContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
let sideInset: CGFloat = 13.0
let bottomInset: CGFloat = 10.0
let contentInset: CGFloat = 9.0
let contentVerticalInset: CGFloat = 11.0
let animationSize: CGSize
let animationInset: CGFloat
let animationSpacing: CGFloat
switch self.icon {
case .none:
animationSize = CGSize()
animationInset = 0.0
animationSpacing = 0.0
case .chatListPress:
animationSize = CGSize(width: 32.0, height: 32.0)
animationInset = (70.0 - animationSize.width) / 2.0
animationSpacing = 8.0
case .info:
animationSize = CGSize(width: 32.0, height: 32.0)
animationInset = 0.0
animationSpacing = 8.0
}
let animationSpacing: CGFloat = 8.0
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - contentInset * 2.0 - sideInset * 2.0 - animationSize.width - animationSpacing, height: .greatestFiniteMagnitude))
let backgroundWidth = textSize.width + contentInset * 2.0 + sideInset * 2.0 + animationSize.width + animationSpacing
let backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0
var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: self.location.minY - bottomInset - backgroundHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: backgroundHeight))
var backgroundFrame = CGRect(origin: CGPoint(x: self.location.midX - backgroundWidth / 2.0, y: self.location.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
if backgroundFrame.minX < sideInset {
backgroundFrame.origin.x = sideInset
}
if backgroundFrame.maxX > layout.size.width - sideInset {
backgroundFrame.origin.x = layout.size.width - sideInset - backgroundFrame.width
}
var invertArrow = false
if backgroundFrame.minY < layout.insets(options: .statusBar).top {
backgroundFrame.origin.y = self.location.maxY + bottomInset
invertArrow = true
}
self.isArrowInverted = invertArrow
transition.updateFrame(node: self.containerNode, frame: backgroundFrame)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
@ -115,7 +170,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: backgroundFrame.height), size: arrowSize)
}
transition.updateFrame(node: self.arrowContainer, frame: arrowFrame)
transition.updateFrame(node: self.arrowContainer, frame: arrowFrame.offsetBy(dx: -backgroundFrame.minX, dy: 0.0))
ContainedViewLayoutTransition.immediate.updateTransformScale(node: self.arrowContainer, scale: CGPoint(x: 1.0, y: invertArrow ? -1.0 : 1.0))
@ -130,13 +185,23 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let event = event {
if let result = self.textNode.hitTest(self.view.convert(point, to: self.textNode.view), with: event) {
return result
}
var eventIsPresses = false
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
eventIsPresses = event.type == .presses
}
if event.type == .touches || eventIsPresses {
if self.shouldDismissOnTouch(point) {
switch self.shouldDismissOnTouch(point) {
case .ignore:
break
case let .dismiss(consume):
self.requestDismiss()
if consume {
return self.view
}
}
return nil
}
@ -146,7 +211,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
func animateIn() {
self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.01)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6)
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.maxY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true)
let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let animationDelay: Double
@ -155,6 +221,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
animationDelay = 0.6
case .info:
animationDelay = 0.2
case .none:
animationDelay = 0.0
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + animationDelay, execute: { [weak self] in
@ -167,7 +235,13 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
completion()
})
self.containerNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.maxY - self.containerNode.bounds.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true)
let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY
self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true)
}
func addRelativeScrollingOffset(_ value: CGFloat) {
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: value)
}
}
@ -177,10 +251,17 @@ public final class TooltipScreen: ViewController {
case chatListPress
}
public enum DismissOnTouch {
case ignore
case dismiss(consume: Bool)
}
private let text: String
private let icon: TooltipScreen.Icon
private let textEntities: [MessageTextEntity]
private let icon: TooltipScreen.Icon?
private let location: CGRect
private let shouldDismissOnTouch: (CGPoint) -> Bool
private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch
private let openUrl: (String) -> Void
private var controllerNode: TooltipScreenNode {
return self.displayNode as! TooltipScreenNode
@ -189,11 +270,15 @@ public final class TooltipScreen: ViewController {
private var validLayout: ContainerViewLayout?
private var isDismissed: Bool = false
public init(text: String, icon: TooltipScreen.Icon, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> Bool) {
public var becameDismissed: ((TooltipScreen) -> Void)?
public init(text: String, textEntities: [MessageTextEntity] = [], icon: TooltipScreen.Icon?, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, openUrl: @escaping (String) -> Void = { _ in }) {
self.text = text
self.textEntities = textEntities
self.icon = icon
self.location = location
self.shouldDismissOnTouch = shouldDismissOnTouch
self.openUrl = openUrl
super.init(navigationBarPresentationData: nil)
@ -209,18 +294,18 @@ public final class TooltipScreen: ViewController {
self.controllerNode.animateIn()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 8.0, execute: { [weak self] in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 20.0, execute: { [weak self] in
self?.dismiss()
})
}
override public func loadDisplayNode() {
self.displayNode = TooltipScreenNode(text: self.text, icon: self.icon, location: self.location, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
self.displayNode = TooltipScreenNode(text: self.text, textEntities: self.textEntities, icon: self.icon, location: self.location, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.dismiss()
})
}, openUrl: self.openUrl)
self.displayNodeDidLoad()
}
@ -237,6 +322,10 @@ public final class TooltipScreen: ViewController {
self.controllerNode.updateLayout(layout: layout, transition: transition)
}
public func addRelativeScrollingOffset(_ value: CGFloat) {
self.controllerNode.addRelativeScrollingOffset(value)
}
override public func dismiss(completion: (() -> Void)? = nil) {
if self.isDismissed {
return
@ -246,7 +335,9 @@ public final class TooltipScreen: ViewController {
guard let strongSelf = self else {
return
}
let becameDismissed = strongSelf.becameDismissed
strongSelf.presentingViewController?.dismiss(animated: false, completion: nil)
becameDismissed?(strongSelf)
})
}
}