Merge commit '3af563fadab2f8bf3b07e763ba8f069238592e6c'

This commit is contained in:
Ali 2021-04-23 18:20:46 +04:00
commit 87fe3d18e6
44 changed files with 6785 additions and 3721 deletions

View File

@ -6456,3 +6456,8 @@ Sorry for the inconvenience.";
"Privacy.PaymentsClear.ShippingInfoCleared" = "Shipping info cleared.";
"Privacy.PaymentsClear.AllInfoCleared" = "Payment and shipping info cleared.";
"Settings.Tips" = "Telegram Features";
"Settings.TipsUsername" = "TelegramTips";
"Calls.NoVoiceAndVideoCallsPlaceholder" = "Your recent voice and video calls will appear here.";
"Calls.StartNewCall" = "Start New Call";

View File

@ -186,6 +186,7 @@ public enum ResolvedUrl {
#endif
case settings(ResolvedUrlSettingsSection)
case joinVoiceChat(PeerId, String?)
case importStickers
}
public enum NavigateToChatKeepStack {

View File

@ -289,10 +289,16 @@ public struct PresentationGroupCallMembers: Equatable {
public final class PresentationGroupCallMemberEvent {
public let peer: Peer
public let isContact: Bool
public let isInChatList: Bool
public let canUnmute: Bool
public let joined: Bool
public init(peer: Peer, joined: Bool) {
public init(peer: Peer, isContact: Bool, isInChatList: Bool, canUnmute: Bool, joined: Bool) {
self.peer = peer
self.isContact = isContact
self.isInChatList = isInChatList
self.canUnmute = canUnmute
self.joined = joined
}
}
@ -341,6 +347,7 @@ public protocol PresentationGroupCall: class {
func lowerHand()
func requestVideo()
func disableVideo()
func switchVideoCamera()
func updateDefaultParticipantsAreMuted(isMuted: Bool)
func setVolume(peerId: PeerId, volume: Int32, sync: Bool)
func setFullSizeVideo(peerId: PeerId?)

View File

@ -27,6 +27,7 @@ swift_library(
"//submodules/PeerOnlineMarkerNode:PeerOnlineMarkerNode",
"//submodules/ContextUI:ContextUI",
"//submodules/TelegramBaseController:TelegramBaseController",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
],
visibility = [
"//visibility:public",

View File

@ -263,6 +263,9 @@ public final class CallListController: TelegramBaseController {
}
}
})
self.controllerNode.startNewCall = { [weak self] in
self?.beginCallImpl()
}
self._ready.set(self.controllerNode.ready)
self.displayNodeDidLoad()
}

View File

@ -13,6 +13,8 @@ import PresentationDataUtils
import AccountContext
import TelegramNotices
import ChatListSearchItemHeader
import AnimatedStickerNode
import AppBundle
private struct CallListNodeListViewTransition {
let callListView: CallListNodeView
@ -179,6 +181,7 @@ final class CallListControllerNode: ASDisplayNode {
var peerSelected: ((PeerId) -> Void)?
var activateSearch: (() -> Void)?
var deletePeerChat: ((PeerId) -> Void)?
var startNewCall: (() -> Void)?
private let viewProcessingQueue = Queue()
private var callListView: CallListNodeView?
@ -196,7 +199,12 @@ final class CallListControllerNode: ASDisplayNode {
private let listNode: ListView
private let leftOverlayNode: ASDisplayNode
private let rightOverlayNode: ASDisplayNode
private let emptyTextNode: ASTextNode
private let emptyTextNode: ImmediateTextNode
private let emptyAnimationNode: AnimatedStickerNode
private var emptyAnimationSize = CGSize()
private let emptyButtonNode: HighlightTrackingButtonNode
private let emptyButtonIconNode: ASImageNode
private let emptyButtonTextNode: ImmediateTextNode
private let call: (PeerId, Bool) -> Void
private let joinGroupCall: (PeerId, CachedChannelData.ActiveCall) -> Void
@ -232,10 +240,26 @@ final class CallListControllerNode: ASDisplayNode {
self.rightOverlayNode = ASDisplayNode()
self.rightOverlayNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor
self.emptyTextNode = ASTextNode()
self.emptyTextNode = ImmediateTextNode()
self.emptyTextNode.alpha = 0.0
self.emptyTextNode.isUserInteractionEnabled = false
self.emptyTextNode.displaysAsynchronously = false
self.emptyTextNode.textAlignment = .center
self.emptyTextNode.maximumNumberOfLines = 3
self.emptyAnimationNode = AnimatedStickerNode()
self.emptyAnimationNode.alpha = 0.0
self.emptyAnimationNode.isUserInteractionEnabled = false
self.emptyButtonNode = HighlightTrackingButtonNode()
self.emptyButtonNode.isUserInteractionEnabled = false
self.emptyButtonTextNode = ImmediateTextNode()
self.emptyButtonTextNode.isUserInteractionEnabled = false
self.emptyButtonIconNode = ASImageNode()
self.emptyButtonIconNode.displaysAsynchronously = false
self.emptyButtonIconNode.isUserInteractionEnabled = false
super.init()
@ -245,6 +269,10 @@ final class CallListControllerNode: ASDisplayNode {
self.addSubnode(self.listNode)
self.addSubnode(self.emptyTextNode)
self.addSubnode(self.emptyAnimationNode)
self.addSubnode(self.emptyButtonTextNode)
self.addSubnode(self.emptyButtonIconNode)
self.addSubnode(self.emptyButtonNode)
switch self.mode {
case .tab:
@ -255,6 +283,31 @@ final class CallListControllerNode: ASDisplayNode {
self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
if let path = getAppBundle().path(forResource: "CallsPlaceholder", ofType: "tgs") {
self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.emptyAnimationSize = CGSize(width: 148.0, height: 148.0)
}
self.emptyButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/CallIcon"), color: presentationData.theme.list.itemAccentColor)
self.emptyButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.emptyButtonIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.emptyButtonIconNode.alpha = 0.4
strongSelf.emptyButtonTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.emptyButtonTextNode.alpha = 0.4
} else {
strongSelf.emptyButtonIconNode.alpha = 1.0
strongSelf.emptyButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.emptyButtonTextNode.alpha = 1.0
strongSelf.emptyButtonTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.emptyButtonNode.addTarget(self, action: #selector(self.emptyButtonPressed), forControlEvents: .touchUpInside)
let nodeInteraction = CallListNodeInteraction(setMessageIdWithRevealedOptions: { [weak self] messageId, fromMessageId in
if let strongSelf = self {
strongSelf.updateState { state in
@ -568,7 +621,7 @@ final class CallListControllerNode: ASDisplayNode {
self.emptyStateDisposable.set((combineLatest(emptySignal, typeSignal, self.statePromise.get()) |> deliverOnMainQueue).start(next: { [weak self] isEmpty, type, state in
if let strongSelf = self {
strongSelf.updateEmptyPlaceholder(theme: state.presentationData.theme, strings: state.presentationData.strings, type: type, hidden: !isEmpty)
strongSelf.updateEmptyPlaceholder(theme: state.presentationData.theme, strings: state.presentationData.strings, type: type, isHidden: !isEmpty)
}
}))
}
@ -594,7 +647,9 @@ final class CallListControllerNode: ASDisplayNode {
self.listNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
}
self.updateEmptyPlaceholder(theme: presentationData.theme, strings: presentationData.strings, type: self.currentLocationAndType.type, hidden: self.emptyTextNode.isHidden)
self.emptyButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call List/CallIcon"), color: presentationData.theme.list.itemAccentColor)
self.updateEmptyPlaceholder(theme: presentationData.theme, strings: presentationData.strings, type: self.currentLocationAndType.type, isHidden: self.emptyTextNode.alpha.isZero)
self.updateState {
return $0.withUpdatedPresentationData(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: presentationData.disableAnimations)
@ -609,20 +664,40 @@ final class CallListControllerNode: ASDisplayNode {
}
private let textFont = Font.regular(16.0)
private let buttonFont = Font.regular(17.0)
func updateEmptyPlaceholder(theme: PresentationTheme, strings: PresentationStrings, type: CallListViewType, hidden: Bool) {
let alpha: CGFloat = hidden ? 0.0 : 1.0
func updateEmptyPlaceholder(theme: PresentationTheme, strings: PresentationStrings, type: CallListViewType, isHidden: Bool) {
let alpha: CGFloat = isHidden ? 0.0 : 1.0
let previousAlpha = self.emptyTextNode.alpha
self.emptyTextNode.alpha = alpha
self.emptyTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2)
if !hidden {
if previousAlpha.isZero && !alpha.isZero {
self.emptyAnimationNode.visibility = true
}
self.emptyAnimationNode.alpha = alpha
self.emptyAnimationNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2, completion: { [weak self] _ in
if let strongSelf = self {
if !previousAlpha.isZero && alpha.isZero {
strongSelf.emptyAnimationNode.visibility = false
}
}
})
self.emptyButtonIconNode.alpha = alpha
self.emptyButtonIconNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2)
self.emptyButtonTextNode.alpha = alpha
self.emptyButtonTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2)
self.emptyButtonNode.isUserInteractionEnabled = !isHidden
if !isHidden {
let type = self.currentLocationAndType.type
let string: String
let emptyText: String
let buttonText = strings.Calls_StartNewCall
if type == .missed {
string = strings.Calls_NoMissedCallsPlacehoder
emptyText = strings.Calls_NoMissedCallsPlacehoder
} else {
string = strings.Calls_NoCallsPlaceholder
emptyText = strings.Calls_NoVoiceAndVideoCallsPlaceholder
}
let color: UIColor
@ -637,7 +712,10 @@ final class CallListControllerNode: ASDisplayNode {
color = theme.list.freeTextColor
}
self.emptyTextNode.attributedText = NSAttributedString(string: string, font: textFont, textColor: color, paragraphAlignment: .center)
self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: textFont, textColor: color, paragraphAlignment: .center)
self.emptyButtonTextNode.attributedText = NSAttributedString(string: buttonText, font: buttonFont, textColor: theme.list.itemAccentColor, paragraphAlignment: .center)
if let layout = self.containerLayout {
self.updateLayout(layout.0, navigationBarHeight: layout.1, transition: .immediate)
}
@ -730,6 +808,10 @@ final class CallListControllerNode: ASDisplayNode {
}
}
@objc private func emptyButtonPressed() {
self.startNewCall?()
}
func updateLayout(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [.input])
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
@ -742,8 +824,30 @@ final class CallListControllerNode: ASDisplayNode {
let size = layout.size
let contentRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
let textSize = self.emptyTextNode.measure(CGSize(width: size.width - 20.0, height: size.height))
transition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: contentRect.minY + floor((contentRect.height - textSize.height) / 2.0)), size: textSize))
let sideInset: CGFloat = 64.0
let emptyAnimationHeight = self.emptyAnimationSize.height
let emptyAnimationSpacing: CGFloat = 13.0
let emptyTextSpacing: CGFloat = 23.0
let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: contentRect.width - sideInset * 2.0, height: size.height))
let emptyButtonSize = self.emptyButtonTextNode.updateLayout(CGSize(width: contentRect.width - sideInset * 2.0, height: size.height))
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing + emptyButtonSize.height
let emptyAnimationY = contentRect.minY + floorToScreenPixels((contentRect.height - emptyTotalHeight) / 2.0)
let textTransition = ContainedViewLayoutTransition.immediate
textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + (contentRect.width - self.emptyAnimationSize.width) / 2.0, y: emptyAnimationY), size: self.emptyAnimationSize))
textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + (contentRect.width - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTextSize))
let emptyButtonSpacing: CGFloat = 14.0
let emptyButtonIconSize = (self.emptyButtonIconNode.image?.size ?? CGSize())
let emptyButtonWidth = emptyButtonIconSize.width + emptyButtonSpacing + emptyButtonSize.width
let emptyButtonX = floor(contentRect.width - emptyButtonWidth) / 2.0
textTransition.updateFrame(node: self.emptyButtonIconNode, frame: CGRect(origin: CGPoint(x: emptyButtonX, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing), size: emptyButtonIconSize))
textTransition.updateFrame(node: self.emptyButtonTextNode, frame: CGRect(origin: CGPoint(x: emptyButtonX + emptyButtonIconSize.width + emptyButtonSpacing, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing + 4.0), size: emptyButtonSize))
textTransition.updateFrame(node: self.emptyButtonNode, frame: CGRect(origin: CGPoint(x: emptyButtonX, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTextSize.height + emptyTextSpacing), size: CGSize(width: emptyButtonWidth, height: 44.0)))
self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {

View File

@ -689,7 +689,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode
private let emptyResultsAnimationNode: AnimatedStickerNode
private var animationSize: CGSize = CGSize()
private var emptyResultsAnimationSize: CGSize = CGSize()
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData)?
@ -792,8 +792,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
super.init()
if let path = getAppBundle().path(forResource: "ChatListNoResults", ofType: "tgs") {
self.emptyResultsAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.animationSize = CGSize(width: 124.0, height: 124.0)
self.emptyResultsAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.emptyResultsAnimationSize = CGSize(width: 148.0, height: 148.0)
}
self.addSubnode(self.recentListNode)
@ -1935,21 +1935,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyAnimationHeight = self.animationSize.height
let emptyAnimationHeight = self.emptyResultsAnimationSize.height
let emptyAnimationSpacing: CGFloat = 8.0
// if case .landscape = layout.orientation, case .compact = layout.metrics.widthClass {
// emptyAnimationHeight = 0.0
// emptyAnimationSpacing = 0.0
// }
let emptyTextSpacing: CGFloat = 8.0
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing
let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0)
let textTransition = ContainedViewLayoutTransition.immediate
textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.animationSize.width) / 2.0, y: emptyAnimationY), size: self.animationSize))
textTransition.updateFrame(node: self.emptyResultsAnimationNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - self.emptyResultsAnimationSize.width) / 2.0, y: emptyAnimationY), size: self.emptyResultsAnimationSize))
textTransition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize))
textTransition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
self.emptyResultsAnimationNode.updateLayout(size: self.animationSize)
self.emptyResultsAnimationNode.updateLayout(size: self.emptyResultsAnimationSize)
if !hadValidLayout {
while !self.enqueuedRecentTransitions.isEmpty {

View File

@ -168,6 +168,7 @@ public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerD
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
public var animatedOut: (() -> Void)?
var deactivate: (() -> Void)?
public var deactivated: (() -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
override public init() {
@ -196,6 +197,7 @@ public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerD
strongSelf.isActive = false
strongSelf.deactivate?()
strongSelf.deactivated?()
}
self.gesture.updated = { [weak self] scale, pinchLocation, offset in

View File

@ -0,0 +1,35 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ImportStickerPackUI",
module_name = "ImportStickerPackUI",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SyncCore:SyncCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/ShareController:ShareController",
"//submodules/ItemListUI:ItemListUI",
"//submodules/StickerResources:StickerResources",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TextFormat:TextFormat",
"//submodules/MergeLists:MergeLists",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/UndoUI:UndoUI",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,49 @@
import Foundation
import UIKit
public class ImportStickerPack {
public class Sticker: Equatable {
public static func == (lhs: ImportStickerPack.Sticker, rhs: ImportStickerPack.Sticker) -> Bool {
return lhs.uuid == rhs.uuid
}
let image: UIImage
let emojis: [String]
let uuid: UUID
init(image: UIImage, emojis: [String], uuid: UUID = UUID()) {
self.image = image
self.emojis = emojis
self.uuid = uuid
}
}
public var identifier: String
public var name: String
public let software: String
public var thumbnail: String?
public let isAnimated: Bool
public var stickers: [Sticker]
public init?(data: Data) {
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
return nil
}
self.name = json["name"] as? String ?? ""
self.identifier = json["identifier"] as? String ?? ""
self.software = json["software"] as? String ?? ""
self.isAnimated = json["isAnimated"] as? Bool ?? false
var stickers: [Sticker] = []
if let stickersArray = json["stickers"] as? [[String: Any]] {
for sticker in stickersArray {
if let dataString = sticker["data"] as? String, let data = Data(base64Encoded: dataString), let image = UIImage(data: data) {
stickers.append(Sticker(image: image, emojis: sticker["emojis"] as? [String] ?? []))
}
}
}
self.stickers = stickers
}
}

View File

@ -0,0 +1,107 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
import TelegramUIPreferences
import AccountContext
import ShareController
import StickerResources
import AlertUI
import PresentationDataUtils
import UndoUI
public final class ImportStickerPackController: ViewController, StandalonePresentableController {
private var controllerNode: ImportStickerPackControllerNode {
return self.displayNode as! ImportStickerPackControllerNode
}
private var animatedIn = false
private var isDismissed = false
public var dismissed: (() -> Void)?
private let context: AccountContext
private weak var parentNavigationController: NavigationController?
private let stickerPack: ImportStickerPack
private var presentationDataDisposable: Disposable?
public init(context: AccountContext, stickerPack: ImportStickerPack, parentNavigationController: NavigationController?) {
self.context = context
self.parentNavigationController = parentNavigationController
self.stickerPack = stickerPack
super.init(navigationBarPresentationData: nil)
self.blocksBackgroundWhenInOverlay = true
self.acceptsFocusWhenInOverlay = true
self.statusBar.statusBarStyle = .Ignore
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self, strongSelf.isNodeLoaded {
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = ImportStickerPackControllerNode(context: self.context)
self.controllerNode.dismiss = { [weak self] in
self?.dismissed?()
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.controllerNode.cancel = { [weak self] in
self?.dismiss()
}
self.controllerNode.presentInGlobalOverlay = { [weak self] controller, arguments in
self?.presentInGlobalOverlay(controller, with: arguments)
}
Queue.mainQueue().after(0.1) {
self.controllerNode.updateStickerPack(self.stickerPack)
}
self.ready.set(self.controllerNode.ready.get())
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.animatedIn {
self.animatedIn = true
self.controllerNode.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
} else {
return
}
self.acceptsFocusWhenInOverlay = false
self.requestUpdateParameters()
self.controllerNode.animateOut(completion: completion)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
}

View File

@ -0,0 +1,554 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import ActivityIndicator
import TextFormat
import AccountContext
private struct StickerPackPreviewGridEntry: Comparable, Identifiable {
let index: Int
let stickerItem: ImportStickerPack.Sticker
var stableId: Int {
return self.index
// return self.stickerItem.file.fileId
}
static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem {
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isEmpty: false)
}
}
private struct StickerPackPreviewGridTransaction {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list)
self.deletions = deleteIndices
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction, theme: theme), previousIndex: $0.2) }
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) }
}
}
final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScrollViewDelegate {
private let context: AccountContext
private var presentationData: PresentationData
private var stickerPack: ImportStickerPack?
private var containerLayout: (ContainerViewLayout, CGFloat)?
private let dimNode: ASDisplayNode
private let wrappingScrollNode: ASScrollNode
private let cancelButtonNode: ASButtonNode
private let contentContainerNode: ASDisplayNode
private let contentBackgroundNode: ASImageNode
private let contentGridNode: GridNode
private let installActionButtonNode: ASButtonNode
private let installActionSeparatorNode: ASDisplayNode
private let contentTitleNode: ImmediateTextNode
private let contentSeparatorNode: ASDisplayNode
private var interaction: StickerPackPreviewInteraction!
var presentInGlobalOverlay: ((ViewController, Any?) -> Void)?
var dismiss: (() -> Void)?
var cancel: (() -> Void)?
let ready = Promise<Bool>()
private var didSetReady = false
private var currentItems: [StickerPackPreviewGridEntry] = []
private var hapticFeedback: HapticFeedback?
init(context: AccountContext) {
self.context = context
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.wrappingScrollNode = ASScrollNode()
self.wrappingScrollNode.view.alwaysBounceVertical = true
self.wrappingScrollNode.view.delaysContentTouches = false
self.wrappingScrollNode.view.canCancelContentTouches = true
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.cancelButtonNode = ASButtonNode()
self.cancelButtonNode.displaysAsynchronously = false
self.contentContainerNode = ASDisplayNode()
self.contentContainerNode.isOpaque = false
self.contentContainerNode.clipsToBounds = true
self.contentBackgroundNode = ASImageNode()
self.contentBackgroundNode.displaysAsynchronously = false
self.contentBackgroundNode.displayWithoutProcessing = true
self.contentGridNode = GridNode()
self.installActionButtonNode = HighlightTrackingButtonNode()
self.installActionButtonNode.displaysAsynchronously = false
self.installActionButtonNode.titleNode.displaysAsynchronously = false
self.contentTitleNode = ImmediateTextNode()
self.contentTitleNode.displaysAsynchronously = false
self.contentTitleNode.maximumNumberOfLines = 1
self.contentSeparatorNode = ASDisplayNode()
self.contentSeparatorNode.isLayerBacked = true
self.installActionSeparatorNode = ASDisplayNode()
self.installActionSeparatorNode.isLayerBacked = true
self.installActionSeparatorNode.displaysAsynchronously = false
super.init()
self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: false)
self.backgroundColor = nil
self.isOpaque = false
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.addSubnode(self.dimNode)
self.wrappingScrollNode.view.delegate = self
self.addSubnode(self.wrappingScrollNode)
self.wrappingScrollNode.addSubnode(self.cancelButtonNode)
self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.installActionButtonNode.addTarget(self, action: #selector(self.installActionButtonPressed), forControlEvents: .touchUpInside)
self.wrappingScrollNode.addSubnode(self.contentBackgroundNode)
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
self.contentContainerNode.addSubnode(self.contentGridNode)
self.contentContainerNode.addSubnode(self.installActionSeparatorNode)
self.contentContainerNode.addSubnode(self.installActionButtonNode)
self.wrappingScrollNode.addSubnode(self.contentTitleNode)
self.wrappingScrollNode.addSubnode(self.contentSeparatorNode)
self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in
self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition)
}
}
override func didLoad() {
super.didLoad()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.contentGridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in
if let strongSelf = self {
if let itemNode = strongSelf.contentGridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem {
// var menuItems: [PeekControllerMenuItem] = []
// if let stickerPack = strongSelf.stickerPack, case let .result(info, _, _) = stickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
// if strongSelf.sendSticker != nil {
// menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in
// if let strongSelf = self {
// return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false
// } else {
// return false
// }
// }))
// }
// menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in
// if let strongSelf = self {
// if isStarred {
// let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
// } else {
// let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
// }
// }
// return true
// }))
// menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true }))
// }
// return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems))
return .single((itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: [])))
}
}
return nil
}, present: { [weak self] content, sourceNode in
if let strongSelf = self {
let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.presentationData.theme), content: content, sourceNode: {
return sourceNode
})
controller.visibilityUpdated = { [weak self] visible in
if let strongSelf = self {
strongSelf.contentGridNode.forceHidden = visible
}
}
strongSelf.presentInGlobalOverlay?(controller, nil)
return controller
}
return nil
}, updateContent: { [weak self] content in
if let strongSelf = self {
var item: ImportStickerPack.Sticker?
if let content = content as? StickerPreviewPeekContent {
item = content.item
}
strongSelf.updatePreviewingItem(item: item, animated: true)
}
}, activateBySingleTap: true))
self.updatePresentationData(self.presentationData)
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
let theme = presentationData.theme
let solidBackground = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1)
let highlightedSolidBackground = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1)
let halfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0)))
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1)
let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.actionSheet.opaqueItemHighlightedBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0)))
})?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1)
let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: presentationData.theme.actionSheet.opaqueItemBackgroundColor)
let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: presentationData.theme.actionSheet.opaqueItemHighlightedBackgroundColor)
self.contentBackgroundNode.image = roundedBackground
self.cancelButtonNode.setBackgroundImage(roundedBackground, for: .normal)
self.cancelButtonNode.setBackgroundImage(highlightedRoundedBackground, for: .highlighted)
self.installActionButtonNode.setBackgroundImage(halfRoundedBackground, for: .normal)
self.installActionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted)
self.contentSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.installActionSeparatorNode.backgroundColor = presentationData.theme.actionSheet.opaqueItemSeparatorColor
self.cancelButtonNode.setTitle(presentationData.strings.Common_Cancel, with: Font.medium(20.0), with: presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
self.contentTitleNode.linkHighlightColor = presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5)
if let (layout, navigationBarHeight) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var insets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top)
let cleanInsets = layout.insets(options: [.statusBar])
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
if insets.bottom > 0 {
bottomInset -= 12.0
}
let buttonHeight: CGFloat = 57.0
let sectionSpacing: CGFloat = 8.0
let titleAreaHeight: CGFloat = 51.0
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 10.0 + layout.safeInsets.left)
let sideInset = floor((layout.size.width - width) / 2.0)
transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: width, height: buttonHeight)))
let maximumContentHeight = layout.size.height - insets.top - bottomInset - buttonHeight - sectionSpacing
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
let contentFrame = contentContainerFrame.insetBy(dx: 12.0, dy: 0.0)
var transaction: StickerPackPreviewGridTransaction?
var itemCount = 0
var animateIn = false
if let stickerPack = self.stickerPack {
var updatedItems: [StickerPackPreviewGridEntry] = []
for item in stickerPack.stickers {
updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item))
}
if self.currentItems.isEmpty && !updatedItems.isEmpty {
let entities = generateTextEntities(stickerPack.name, enabledTypes: [.mention])
let font = Font.medium(20.0)
self.contentTitleNode.attributedText = stringWithAppliedEntities(stickerPack.name, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font)
animateIn = true
itemCount = updatedItems.count
}
transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme)
self.currentItems = updatedItems
}
let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
let titleFrame = CGRect(origin: CGPoint(x: contentContainerFrame.minX + floor((contentContainerFrame.size.width - titleSize.width) / 2.0), y: self.contentBackgroundNode.frame.minY + 15.0), size: titleSize)
let deltaTitlePosition = CGPoint(x: titleFrame.midX - self.contentTitleNode.frame.midX, y: titleFrame.midY - self.contentTitleNode.frame.midY)
self.contentTitleNode.frame = titleFrame
transition.animatePosition(node: self.contentTitleNode, from: CGPoint(x: titleFrame.midX + deltaTitlePosition.x, y: titleFrame.midY + deltaTitlePosition.y))
transition.updateFrame(node: self.contentTitleNode, frame: titleFrame)
transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
let itemsPerRow = 4
let itemWidth = floor(contentFrame.size.width / CGFloat(itemsPerRow))
let rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0)
let minimallyRevealedRowCount: CGFloat = 3.5
let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount))
let bottomGridInset = buttonHeight
let topInset = max(0.0, contentFrame.size.height - initiallyRevealedRowCount * itemWidth - titleAreaHeight - bottomGridInset)
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
let installButtonOffset = buttonHeight
transition.updateFrame(node: self.installActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - installButtonOffset), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight)))
transition.updateFrame(node: self.installActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - installButtonOffset - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight))
self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transaction?.deletions ?? [], insertItems: transaction?.insertions ?? [], updateItems: transaction?.updates ?? [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize))
if animateIn {
self.contentGridNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.installActionButtonNode.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.installActionSeparatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
private func gridPresentationLayoutUpdated(_ presentationLayout: GridNodeCurrentPresentationLayout, transition: ContainedViewLayoutTransition) {
if let (layout, _) = self.containerLayout {
var insets = layout.insets(options: [.statusBar])
insets.top = max(10.0, insets.top)
let cleanInsets = layout.insets(options: [.statusBar])
var bottomInset: CGFloat = 10.0 + cleanInsets.bottom
if insets.bottom > 0 {
bottomInset -= 12.0
}
let buttonHeight: CGFloat = 57.0
let sectionSpacing: CGFloat = 8.0
let titleAreaHeight: CGFloat = 51.0
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 10.0 + layout.safeInsets.left)
let sideInset = floor((layout.size.width - width) / 2.0)
let maximumContentHeight = layout.size.height - insets.top - bottomInset - buttonHeight - sectionSpacing
let contentFrame = CGRect(origin: CGPoint(x: sideInset, y: insets.top), size: CGSize(width: width, height: maximumContentHeight))
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY - presentationLayout.contentOffset.y), size: contentFrame.size)
if backgroundFrame.minY < contentFrame.minY {
backgroundFrame.origin.y = contentFrame.minY
}
if backgroundFrame.maxY > contentFrame.maxY {
backgroundFrame.size.height += contentFrame.maxY - backgroundFrame.maxY
}
if backgroundFrame.size.height < buttonHeight + 32.0 {
backgroundFrame.origin.y -= buttonHeight + 32.0 - backgroundFrame.size.height
backgroundFrame.size.height = buttonHeight + 32.0
}
var compactFrame = false
let backgroundDeltaY = backgroundFrame.minY - self.contentBackgroundNode.frame.minY
transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame)
transition.animatePositionAdditive(node: self.contentGridNode, offset: CGPoint(x: 0.0, y: -backgroundDeltaY))
let titleSize = self.contentTitleNode.bounds.size
let titleFrame = CGRect(origin: CGPoint(x: contentFrame.minX + floor((contentFrame.size.width - titleSize.width) / 2.0), y: backgroundFrame.minY + 15.0), size: titleSize)
transition.updateFrame(node: self.contentTitleNode, frame: titleFrame)
transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentFrame.minX, y: backgroundFrame.minY + titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: UIScreenPixel)))
if !compactFrame && CGFloat(0.0).isLessThanOrEqualTo(presentationLayout.contentOffset.y) {
self.contentSeparatorNode.alpha = 1.0
} else {
self.contentSeparatorNode.alpha = 0.0
}
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.cancelButtonPressed()
}
}
@objc func cancelButtonPressed() {
self.cancel?()
}
@objc func installActionButtonPressed() {
}
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
var dimCompleted = false
var offsetCompleted = false
let internalCompletion: () -> Void = { [weak self] in
if let strongSelf = self, dimCompleted && offsetCompleted {
strongSelf.dismiss?()
}
completion?()
}
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
dimCompleted = true
internalCompletion()
})
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position
self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true
internalCompletion()
})
}
func updateStickerPack(_ stickerPack: ImportStickerPack) {
self.stickerPack = stickerPack
// self.interaction.playAnimatedStickers = stickerSettings.loopAnimatedStickers
if let _ = self.containerLayout {
self.dequeueUpdateStickerPack()
}
self.installActionButtonNode.setTitle("Create Sticker Set", with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
// switch stickerPack {
// case .none, .fetching:
// self.installActionSeparatorNode.alpha = 0.0
// self.shareActionSeparatorNode.alpha = 0.0
// self.shareActionButtonNode.alpha = 0.0
// self.installActionButtonNode.alpha = 0.0
// self.installActionButtonNode.setTitle("", with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal)
// case let .result(info, _, installed):
// if self.stickerPackInitiallyInstalled == nil {
// self.stickerPackInitiallyInstalled = installed
// }
// self.installActionSeparatorNode.alpha = 1.0
// self.shareActionSeparatorNode.alpha = 1.0
// self.shareActionButtonNode.alpha = 1.0
// self.installActionButtonNode.alpha = 1.0
// if installed {
// let text: String
// if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
// text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count)
// } else {
// text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count)
// }
// self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.destructiveActionTextColor, for: .normal)
// } else {
// let text: String
// if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks {
// text = self.presentationData.strings.StickerPack_AddStickerCount(info.count)
// } else {
// text = self.presentationData.strings.StickerPack_AddMaskCount(info.count)
// }
// self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
// }
// }
}
func dequeueUpdateStickerPack() {
if let (layout, navigationBarHeight) = self.containerLayout, let _ = self.stickerPack {
let transition: ContainedViewLayoutTransition
if self.didSetReady {
transition = .animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
if !self.didSetReady {
self.didSetReady = true
self.ready.set(.single(true))
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.installActionButtonNode.hitTest(self.installActionButtonNode.convert(point, from: self), with: event) {
return result
}
if self.bounds.contains(point) {
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) {
return self.dimNode.view
}
}
return super.hitTest(point, with: event)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset
let additionalTopHeight = max(0.0, -contentOffset.y)
if additionalTopHeight >= 30.0 {
self.cancelButtonPressed()
}
}
private func updatePreviewingItem(item: ImportStickerPack.Sticker?, animated: Bool) {
if self.interaction.previewedItem !== item {
self.interaction.previewedItem = item
self.contentGridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? StickerPackPreviewGridItemNode {
itemNode.updatePreviewing(animated: animated)
}
}
}
}
}

View File

@ -0,0 +1,255 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SyncCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import StickerResources
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import TelegramPresentationData
import ShimmerEffect
final class StickerPackPreviewInteraction {
var previewedItem: ImportStickerPack.Sticker?
var playAnimatedStickers: Bool
init(playAnimatedStickers: Bool) {
self.playAnimatedStickers = playAnimatedStickers
}
}
final class StickerPackPreviewGridItem: GridItem {
let account: Account
let stickerItem: ImportStickerPack.Sticker
let interaction: StickerPackPreviewInteraction
let theme: PresentationTheme
let isEmpty: Bool
let section: GridSection? = nil
init(account: Account, stickerItem: ImportStickerPack.Sticker, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) {
self.account = account
self.stickerItem = stickerItem
self.interaction = interaction
self.theme = theme
self.isEmpty = isEmpty
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = StickerPackPreviewGridItemNode()
node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isEmpty: self.isEmpty)
return node
}
func update(node: GridItemNode) {
guard let node = node as? StickerPackPreviewGridItemNode else {
assertionFailure()
return
}
node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isEmpty: self.isEmpty)
}
}
private let textFont = Font.regular(20.0)
final class StickerPackPreviewGridItemNode: GridItemNode {
private var currentState: (Account, ImportStickerPack.Sticker?)?
private var isEmpty: Bool?
// private let imageNode: TransformImageNode
private let imageNode: ASImageNode
private var animationNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode?
private var theme: PresentationTheme?
override var isVisibleInGrid: Bool {
didSet {
self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
}
}
private var currentIsPreviewing = false
private let stickerFetchedDisposable = MetaDisposable()
var interaction: StickerPackPreviewInteraction?
var selected: (() -> Void)?
var stickerPackItem: ImportStickerPack.Sticker? {
return self.currentState?.1
}
override init() {
self.imageNode = ASImageNode()
// self.imageNode = TransformImageNode()
// self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode?.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.imageNode)
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.removePlaceholder(animated: !firstTime)
// }
// firstTime = false
// }
}
deinit {
self.stickerFetchedDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
if !animated {
placeholderNode.removeFromSupernode()
} else {
placeholderNode.allowsGroupOpacity = true
placeholderNode.alpha = 0.0
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
placeholderNode?.removeFromSupernode()
placeholderNode?.allowsGroupOpacity = false
})
}
}
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
func setup(account: Account, stickerItem: ImportStickerPack.Sticker?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) {
self.interaction = interaction
self.theme = theme
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 !== stickerItem || self.isEmpty != isEmpty {
if let stickerItem = stickerItem {
self.imageNode.image = stickerItem.image
// if stickerItem.file.isAnimatedSticker {
// let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
// self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
//
// if self.animationNode == nil {
// let animationNode = AnimatedStickerNode()
// self.animationNode = animationNode
// self.addSubnode(animationNode)
// animationNode.started = { [weak self] in
// self?.imageNode.isHidden = true
// self?.removePlaceholder(animated: false)
// }
// }
// let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
// self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
// self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
// self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
// } else {
// if let animationNode = self.animationNode {
// animationNode.visibility = false
// self.animationNode = nil
// animationNode.removeFromSupernode()
// }
// self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
// self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start())
// }
} else {
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
}
}
}
self.currentState = (account, stickerItem)
self.setNeedsLayout()
}
self.isEmpty = isEmpty
}
override func layout() {
super.layout()
let bounds = self.bounds
let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0)
let boundingSize = CGSize(width: boundsSide, height: boundsSide)
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, let (_, stickerItem) = self.currentState, let item = stickerItem {
// placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.file.immediateThumbnailData, size: placeholderFrame.size)
// }
}
self.imageNode.frame = bounds
// if let (_, item) = self.currentState {
// if let item = item, let dimensions = item.file.dimensions?.cgSize {
// let imageSize = dimensions.aspectFitted(boundingSize)
// self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
// self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
// if let animationNode = self.animationNode {
// animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
// animationNode.updateLayout(size: imageSize)
// }
// }
// }
}
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
if let placeholderNode = self.placeholderNode {
placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize)
}
}
func transitionNode() -> ASDisplayNode? {
return self
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
}
func updatePreviewing(animated: Bool) {
var isPreviewing = false
if let (_, maybeItem) = self.currentState, let interaction = self.interaction, let item = maybeItem {
isPreviewing = interaction.previewedItem === item
}
if self.currentIsPreviewing != isPreviewing {
self.currentIsPreviewing = isPreviewing
if isPreviewing {
self.layer.sublayerTransform = CATransform3DMakeScale(0.8, 0.8, 1.0)
if animated {
self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4)
}
} else {
self.layer.sublayerTransform = CATransform3DIdentity
if animated {
self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5)
}
}
}
}
}

View File

@ -0,0 +1,142 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
import StickerResources
import AnimatedStickerNode
import TelegramAnimatedStickerNode
public final class StickerPreviewPeekContent: PeekControllerContent {
let account: Account
public let item: ImportStickerPack.Sticker
let menu: [PeekControllerMenuItem]
public init(account: Account, item: ImportStickerPack.Sticker, menu: [PeekControllerMenuItem]) {
self.account = account
self.item = item
self.menu = menu
}
public func presentation() -> PeekControllerContentPresentation {
return .freeform
}
public func menuActivation() -> PeerkControllerMenuActivation {
return .press
}
public func menuItems() -> [PeekControllerMenuItem] {
return self.menu
}
public func node() -> PeekControllerContentNode & ASDisplayNode {
return StickerPreviewPeekContentNode(account: self.account, item: self.item)
}
public func topAccessoryNode() -> ASDisplayNode? {
return nil
}
public func isEqual(to: PeekControllerContent) -> Bool {
if let to = to as? StickerPreviewPeekContent {
return self.item === to.item
} else {
return false
}
}
}
private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode {
private let account: Account
private let item: ImportStickerPack.Sticker
private var textNode: ASTextNode
private var imageNode: ASImageNode
private var animationNode: AnimatedStickerNode?
private var containerLayout: (ContainerViewLayout, CGFloat)?
init(account: Account, item: ImportStickerPack.Sticker) {
self.account = account
self.item = item
self.textNode = ASTextNode()
self.imageNode = ASImageNode()
self.imageNode.displaysAsynchronously = false
self.imageNode.image = item.image
self.textNode.attributedText = NSAttributedString(string: item.emojis.joined(separator: " "), font: Font.regular(32.0), textColor: .black)
// for case let .Sticker(text, _, _) in item.file.attributes {
// self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black)
// break
// }
// if item.file.isAnimatedSticker {
// let animationNode = AnimatedStickerNode()
// self.animationNode = animationNode
//
// let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
// let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0))
//
// self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
// self.animationNode?.visibility = true
// self.animationNode?.addSubnode(self.textNode)
// } else {
// self.imageNode.addSubnode(self.textNode)
// self.animationNode = nil
// }
// self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false, fetched: true))
super.init()
self.isUserInteractionEnabled = false
if let animationNode = self.animationNode {
self.addSubnode(animationNode)
} else {
self.addSubnode(self.imageNode)
}
self.addSubnode(self.textNode)
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let boundingSize = CGSize(width: 180.0, height: 180.0).fitted(size)
let imageFrame = CGRect(origin: CGPoint(), size: boundingSize)
let textSpacing: CGFloat = 10.0
let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0))
self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize)
self.imageNode.frame = imageFrame
return boundingSize
// if let dimensitons = self.item.file.dimensions {
// let textSpacing: CGFloat = 10.0
// let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0))
//
// let imageSize = dimensitons.cgSize.aspectFitted(boundingSize)
// self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
// let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: textSize.height + textSpacing), size: imageSize)
// self.imageNode.frame = imageFrame
// if let animationNode = self.animationNode {
// animationNode.frame = imageFrame
// animationNode.updateLayout(size: imageSize)
// }
//
// self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0), y: -textSize.height - textSpacing), size: textSize)
//
// return CGSize(width: size.width, height: imageFrame.height + textSize.height + textSpacing)
// } else {
// return CGSize(width: size.width, height: 10.0)
// }
}
}

View File

@ -0,0 +1,447 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SyncCore
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
final class InstantPageSubContentNode : ASDisplayNode {
private let context: AccountContext
private let strings: PresentationStrings
private let nameDisplayOrder: PresentationPersonNameOrder
private let sourcePeerType: MediaAutoDownloadPeerType
private let theme: InstantPageTheme
private let openMedia: (InstantPageMedia) -> Void
private let longPressMedia: (InstantPageMedia) -> Void
private let openPeer: (PeerId) -> Void
private let openUrl: (InstantPageUrlItem) -> Void
var currentLayoutTiles: [InstantPageTile] = []
var currentLayoutItemsWithNodes: [InstantPageItem] = []
var distanceThresholdGroupCount: [Int: Int] = [:]
var visibleTiles: [Int: InstantPageTileNode] = [:]
var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
var currentWebEmbedHeights: [Int : CGFloat] = [:]
var currentExpandedDetails: [Int : Bool]?
var currentDetailsItems: [InstantPageDetailsItem] = []
var requestLayoutUpdate: ((Bool) -> Void)?
var currentLayout: InstantPageLayout
let contentSize: CGSize
let inOverlayPanel: Bool
private var previousVisibleBounds: CGRect?
init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourcePeerType: MediaAutoDownloadPeerType, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) {
self.context = context
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
self.sourcePeerType = sourcePeerType
self.theme = theme
self.openMedia = openMedia
self.longPressMedia = longPressMedia
self.openPeer = openPeer
self.openUrl = openUrl
self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
self.contentSize = contentSize
self.inOverlayPanel = inOverlayPanel
super.init()
self.updateLayout()
}
private func updateLayout() {
for (_, tileNode) in self.visibleTiles {
tileNode.removeFromSupernode()
}
self.visibleTiles.removeAll()
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width)
var currentDetailsItems: [InstantPageDetailsItem] = []
var currentLayoutItemsWithViews: [InstantPageItem] = []
var distanceThresholdGroupCount: [Int: Int] = [:]
var expandedDetails: [Int: Bool] = [:]
var detailsIndex = -1
for item in self.currentLayout.items {
if item.wantsNode {
currentLayoutItemsWithViews.append(item)
if let group = item.distanceThresholdGroup() {
let count: Int
if let currentCount = distanceThresholdGroupCount[Int(group)] {
count = currentCount
} else {
count = 0
}
distanceThresholdGroupCount[Int(group)] = count + 1
}
if let detailsItem = item as? InstantPageDetailsItem {
detailsIndex += 1
expandedDetails[detailsIndex] = detailsItem.initiallyExpanded
currentDetailsItems.append(detailsItem)
}
}
}
if self.currentExpandedDetails == nil {
self.currentExpandedDetails = expandedDetails
}
self.currentLayoutTiles = currentLayoutTiles
self.currentLayoutItemsWithNodes = currentLayoutItemsWithViews
self.currentDetailsItems = currentDetailsItems
self.distanceThresholdGroupCount = distanceThresholdGroupCount
}
var effectiveContentSize: CGSize {
var contentSize = self.contentSize
for item in self.currentDetailsItems {
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
contentSize.height += -item.frame.height + (expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight)
}
return contentSize
}
func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
var visibleTileIndices = Set<Int>()
var visibleItemIndices = Set<Int>()
self.previousVisibleBounds = visibleBounds
var topNode: ASDisplayNode?
let topTileNode = topNode
if let scrollSubnodes = self.subnodes {
for node in scrollSubnodes.reversed() {
if let node = node as? InstantPageTileNode {
topNode = node
break
}
}
}
var collapseOffset: CGFloat = 0.0
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .spring)
} else {
transition = .immediate
}
var itemIndex = -1
var embedIndex = -1
var detailsIndex = -1
for item in self.currentLayoutItemsWithNodes {
itemIndex += 1
if item is InstantPageWebEmbedItem {
embedIndex += 1
}
if item is InstantPageDetailsItem {
detailsIndex += 1
}
var itemThreshold: CGFloat = 0.0
if let group = item.distanceThresholdGroup() {
var count: Int = 0
if let currentCount = self.distanceThresholdGroupCount[group] {
count = currentCount
}
itemThreshold = item.distanceThresholdWithGroupCount(count)
}
var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset)
var thresholdedItemFrame = itemFrame
thresholdedItemFrame.origin.y -= itemThreshold
thresholdedItemFrame.size.height += itemThreshold * 2.0
if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] {
let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight
collapseOffset += itemFrame.height - height
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height))
}
if visibleBounds.intersects(thresholdedItemFrame) {
visibleItemIndices.insert(itemIndex)
var itemNode = self.visibleItemsWithNodes[itemIndex]
if let currentItemNode = itemNode {
if !item.matchesNode(currentItemNode) {
(currentItemNode as! ASDisplayNode).removeFromSupernode()
self.visibleItemsWithNodes.removeValue(forKey: itemIndex)
itemNode = nil
}
}
if itemNode == nil {
let itemIndex = itemIndex
let detailsIndex = detailsIndex
if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in
self?.openMedia(media)
}, longPressMedia: { [weak self] media in
self?.longPressMedia(media)
}, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { [weak self] peerId in
self?.openPeer(peerId)
}, openUrl: { [weak self] url in
self?.openUrl(url)
}, updateWebEmbedHeight: { _ in
}, updateDetailsExpanded: { [weak self] expanded in
self?.updateDetailsExpanded(detailsIndex, expanded)
}, currentExpandedDetails: self.currentExpandedDetails) {
newNode.frame = itemFrame
newNode.updateLayout(size: itemFrame.size, transition: transition)
if let topNode = topNode {
self.insertSubnode(newNode, aboveSubnode: topNode)
} else {
self.insertSubnode(newNode, at: 0)
}
topNode = newNode
self.visibleItemsWithNodes[itemIndex] = newNode
itemNode = newNode
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.requestLayoutUpdate = { [weak self] animated in
self?.requestLayoutUpdate?(animated)
}
}
}
} else {
if (itemNode as! ASDisplayNode).frame != itemFrame {
transition.updateFrame(node: (itemNode as! ASDisplayNode), frame: itemFrame)
itemNode?.updateLayout(size: itemFrame.size, transition: transition)
}
}
if let itemNode = itemNode as? InstantPageDetailsNode {
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
}
}
}
topNode = topTileNode
var tileIndex = -1
for tile in self.currentLayoutTiles {
tileIndex += 1
let tileFrame = effectiveFrameForTile(tile)
var tileVisibleFrame = tileFrame
tileVisibleFrame.origin.y -= 400.0
tileVisibleFrame.size.height += 400.0 * 2.0
if tileVisibleFrame.intersects(visibleBounds) {
visibleTileIndices.insert(tileIndex)
if self.visibleTiles[tileIndex] == nil {
let tileNode = InstantPageTileNode(tile: tile, backgroundColor: self.inOverlayPanel ? self.theme.overlayPanelColor : self.theme.pageBackgroundColor)
tileNode.frame = tileFrame
if let topNode = topNode {
self.insertSubnode(tileNode, aboveSubnode: topNode)
} else {
self.insertSubnode(tileNode, at: 0)
}
topNode = tileNode
self.visibleTiles[tileIndex] = tileNode
} else {
if visibleTiles[tileIndex]!.frame != tileFrame {
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
}
}
}
}
var removeTileIndices: [Int] = []
for (index, tileNode) in self.visibleTiles {
if !visibleTileIndices.contains(index) {
removeTileIndices.append(index)
tileNode.removeFromSupernode()
}
}
for index in removeTileIndices {
self.visibleTiles.removeValue(forKey: index)
}
var removeItemIndices: [Int] = []
for (index, itemNode) in self.visibleItemsWithNodes {
if !visibleItemIndices.contains(index) {
removeItemIndices.append(index)
(itemNode as! ASDisplayNode).removeFromSupernode()
} else {
var itemFrame = (itemNode as! ASDisplayNode).frame
let itemThreshold: CGFloat = 200.0
itemFrame.origin.y -= itemThreshold
itemFrame.size.height += itemThreshold * 2.0
itemNode.updateIsVisible(visibleBounds.intersects(itemFrame))
}
}
for index in removeItemIndices {
self.visibleItemsWithNodes.removeValue(forKey: index)
}
}
private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
// let currentHeight = self.currentWebEmbedHeights[index]
// if height != currentHeight {
// if let currentHeight = currentHeight, currentHeight > height {
// return
// }
// self.currentWebEmbedHeights[index] = height
//
// let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
// self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in
// if let strongSelf = self {
// strongSelf.updateLayout()
// strongSelf.updateVisibleItems()
// }
// }))
// }
}
func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) {
if var currentExpandedDetails = self.currentExpandedDetails {
currentExpandedDetails[index] = expanded
self.currentExpandedDetails = currentExpandedDetails
}
self.requestLayoutUpdate?(animated)
}
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
for (_, itemNode) in self.visibleItemsWithNodes {
if let transitionNode = itemNode.transitionNode(media: media) {
return transitionNode
}
}
return nil
}
func updateHiddenMedia(media: InstantPageMedia?) {
for (_, itemNode) in self.visibleItemsWithNodes {
itemNode.updateHiddenMedia(media: media)
}
}
func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint {
var contentOffset = CGPoint()
for (_, itemNode) in self.visibleItemsWithNodes {
if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item {
contentOffset = itemNode.contentOffset
break
}
}
return contentOffset
}
func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? {
for (_, itemNode) in self.visibleItemsWithNodes {
if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item {
return detailsNode
}
}
return nil
}
private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize {
if let node = nodeForDetailsItem(item) {
return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight)
} else {
return item.frame.size
}
}
private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect {
let layoutOrigin = tile.frame.origin
var origin = layoutOrigin
for item in self.currentDetailsItems {
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
if layoutOrigin.y >= item.frame.maxY {
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
origin.y += height - item.frame.height
}
}
return CGRect(origin: origin, size: tile.frame.size)
}
func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect {
let layoutOrigin = item.frame.origin
var origin = layoutOrigin
for item in self.currentDetailsItems {
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
if layoutOrigin.y >= item.frame.maxY {
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
origin.y += height - item.frame.height
}
}
if let item = item as? InstantPageDetailsItem {
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height))
} else {
return CGRect(origin: origin, size: item.frame.size)
}
}
func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
for item in self.currentLayout.items {
let itemFrame = self.effectiveFrameForItem(item)
if itemFrame.contains(location) {
if let item = item as? InstantPageTextItem, item.selectable {
return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY))
} else if let item = item as? InstantPageScrollableItem {
let contentOffset = scrollableContentOffset(item: item)
if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) {
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y))
}
} else if let item = item as? InstantPageDetailsItem {
for (_, itemNode) in self.visibleItemsWithNodes {
if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) {
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y))
}
}
}
}
}
}
return nil
}
func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
for item in self.currentLayout.items {
let frame = self.effectiveFrameForItem(item)
if frame.contains(point) {
if item is InstantPagePeerReferenceItem {
return .fail
} else if item is InstantPageAudioItem {
return .fail
} else if item is InstantPageArticleItem {
return .fail
} else if item is InstantPageFeedbackItem {
return .fail
} else if let item = item as? InstantPageDetailsItem {
for (_, itemNode) in self.visibleItemsWithNodes {
if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY))
}
}
}
break
}
}
return .waitForSingleTap
}
}

View File

@ -78,12 +78,33 @@ private class PeerInfoAvatarListLoadingStripNode: ASImageNode {
}
}
private struct CustomListItemResourceId: MediaResourceId {
public var uniqueId: String {
return "customNode"
}
public var hashValue: Int {
return 0
}
public func isEqual(to: MediaResourceId) -> Bool {
if to is CustomListItemResourceId {
return true
} else {
return false
}
}
}
public enum PeerInfoAvatarListItem: Equatable {
case custom(ASDisplayNode)
case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?)
case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?)
var id: WrappedMediaResourceId {
switch self {
case .custom:
return WrappedMediaResourceId(CustomListItemResourceId())
case let .topImage(representations, _, _):
let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation
return WrappedMediaResourceId(representation.resource.id)
@ -95,6 +116,8 @@ public enum PeerInfoAvatarListItem: Equatable {
var videoRepresentations: [VideoRepresentationWithReference] {
switch self {
case .custom:
return []
case let .topImage(_, videoRepresentations, _):
return videoRepresentations
case let .image(_, _, videoRepresentations, _):
@ -330,6 +353,11 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
let immediateThumbnailData: Data?
var id: Int64
switch item {
case let .custom(node):
id = 0
representations = []
videoRepresentations = []
immediateThumbnailData = nil
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations
videoRepresentations = videoRepresentationsValue

View File

@ -22,7 +22,7 @@ public struct WrappedMediaResourceId: Hashable {
// }
public func hash(into hasher: inout Hasher) {
hasher.combine(id.hashValue)
hasher.combine(self.id.hashValue)
}
}

View File

@ -546,7 +546,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var toneRenderer: PresentationCallToneRenderer?
private var videoCapturer: OngoingCallVideoCapturer?
private var useFrontCamera: Bool = true
private let incomingVideoSourcePromise = Promise<[PeerId: UInt32]>([:])
public var incomingVideoSources: Signal<[PeerId: UInt32], NoError> {
return self.incomingVideoSourcePromise.get()
@ -1908,7 +1908,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|> mapToSignal { event -> Signal<PresentationGroupCallMemberEvent, NoError> in
return postbox.transaction { transaction -> Signal<PresentationGroupCallMemberEvent, NoError> in
if let peer = transaction.getPeer(event.peerId) {
return .single(PresentationGroupCallMemberEvent(peer: peer, joined: event.joined))
let isContact = transaction.isPeerContact(peerId: event.peerId)
let isInChatList = transaction.getPeerChatListIndex(event.peerId) != nil
return .single(PresentationGroupCallMemberEvent(peer: peer, isContact: isContact, isInChatList: isInChatList, canUnmute: event.canUnmute, joined: event.joined))
} else {
return .complete()
}
@ -1916,13 +1918,20 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|> switchToLatest
}
|> deliverOnMainQueue).start(next: { [weak self] event in
guard let strongSelf = self else {
guard let strongSelf = self, event.peer.id != strongSelf.stateValue.myPeerId else {
return
}
if event.peer.id == strongSelf.stateValue.myPeerId {
return
var skip = false
if let participantsCount = strongSelf.participantsContext?.immediateState?.totalCount, participantsCount >= 250 {
if event.peer.isVerified || event.isContact || event.isInChatList || (strongSelf.stateValue.defaultParticipantMuteState == .muted && event.canUnmute) {
skip = false
} else {
skip = true
}
}
if !skip {
strongSelf.memberEventsPipe.putNext(event)
}
strongSelf.memberEventsPipe.putNext(event)
}))
if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting {
@ -2332,6 +2341,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
public func switchVideoCamera() {
self.useFrontCamera = !self.useFrontCamera
self.videoCapturer?.switchVideoInput(isFront: self.useFrontCamera)
}
public func setVolume(peerId: PeerId, volume: Int32, sync: Bool) {
for (ssrc, id) in self.ssrcMapping {
if id == peerId {

View File

@ -68,9 +68,9 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
private var currentParams: (size: CGSize, buttonSize: CGSize, state: VoiceChatActionButton.State, dark: Bool, small: Bool, title: String, subtitle: String, snap: Bool)?
private var activePromise = ValuePromise<Bool>(false)
private var outerColorPromise = ValuePromise<UIColor?>(nil)
var outerColor: Signal<UIColor?, NoError> {
return outerColorPromise.get()
private var outerColorPromise = Promise<(UIColor?, UIColor?)>((nil, nil))
var outerColor: Signal<(UIColor?, UIColor?), NoError> {
return self.outerColorPromise.get()
}
var connectingColor: UIColor = UIColor(rgb: 0xb6b6bb) {
@ -195,8 +195,8 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self?.activePromise.set(active)
}
self.backgroundNode.updatedOuterColor = { [weak self] color in
self?.outerColorPromise.set(color)
self.backgroundNode.updatedColors = { [weak self] outerColor, activeColor in
self?.outerColorPromise.set(.single((outerColor, activeColor)))
}
}
@ -273,16 +273,17 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 0.0)
} else {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
if small {
transition.updateTransformScale(node: self.backgroundNode, scale: self.pressing ? smallScale * 0.9 : smallScale, delay: 0.05)
transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? smallIconScale * 0.9 : smallIconScale, delay: 0.05)
transition.updateAlpha(node: self.titleLabel, alpha: 0.0)
transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0)
} else {
transition.updateTransformScale(node: self.backgroundNode, scale: 1.0, delay: 0.05)
transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? 0.9 : 1.0, delay: 0.05)
transition.updateAlpha(node: self.titleLabel, alpha: 1.0, delay: 0.05)
transition.updateAlpha(node: self.subtitleLabel, alpha: 1.0, delay: 0.05)
}
transition.updateAlpha(node: self.titleLabel, alpha: 1.0, delay: 0.05)
transition.updateAlpha(node: self.subtitleLabel, alpha: 1.0, delay: 0.05)
transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 1.0)
}
@ -293,7 +294,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
private var previousIcon: VoiceChatActionButtonIconAnimationState?
private func applyIconParams() {
guard let (_, _, state, _, _, _, _, snap) = self.currentParams else {
guard let (_, _, state, _, _, _, _, _) = self.currentParams else {
return
}
@ -336,7 +337,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.currentParams = (previous.size, previous.buttonSize, previous.state, previous.dark, previous.small, previous.title, previous.subtitle, snap)
self.backgroundNode.isSnap = snap
self.backgroundNode.glowHidden = snap
self.backgroundNode.glowHidden = snap || previous.small
self.backgroundNode.updateColors()
self.applyParams(animated: animated)
self.applyIconParams()
@ -388,6 +389,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
}
self.applyIconParams()
self.backgroundNode.glowHidden = (self.currentParams?.snap ?? false) || small
self.backgroundNode.isDark = dark
self.backgroundNode.update(state: backgroundState, animated: animated)
@ -533,7 +535,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
}
var updatedActive: ((Bool) -> Void)?
var updatedOuterColor: ((UIColor?) -> Void)?
var updatedColors: ((UIColor?, UIColor?) -> Void)?
private let backgroundCircleLayer = CAShapeLayer()
private let foregroundCircleLayer = CAShapeLayer()
@ -773,6 +775,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
let initialColors = self.foregroundGradientLayer.colors
let outerColor: UIColor?
let activeColor: UIColor?
let targetColors: [CGColor]
let targetScale: CGFloat
switch type {
@ -780,20 +783,24 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor]
targetScale = 0.89
outerColor = UIColor(rgb: 0x21674f)
activeColor = green
case .active:
targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
targetScale = 0.85
outerColor = UIColor(rgb: 0x1d588d)
activeColor = blue
case .connecting:
targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
targetScale = 0.3
outerColor = nil
activeColor = blue
case .muted:
targetColors = [pink.cgColor, purple.cgColor, purple.cgColor]
targetScale = 0.85
outerColor = UIColor(rgb: 0x3b3474)
activeColor = purple
}
self.updatedOuterColor?(outerColor)
self.updatedColors?(outerColor, activeColor)
self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0)
if let _ = previousType {

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ import Postbox
import AccountContext
import TelegramPresentationData
import AvatarNode
import AnimationUI
func optionsBackgroundImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
@ -30,6 +31,15 @@ func optionsButtonImage(dark: Bool) -> UIImage? {
})
}
func optionsCircleImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: dark ? 0x1c1c1e : 0x2c2c2e).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})
}
func closeButtonImage(dark: Bool) -> UIImage? {
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
@ -51,6 +61,7 @@ func closeButtonImage(dark: Bool) -> UIImage? {
final class VoiceChatHeaderButton: HighlightableButtonNode {
enum Content {
case image(UIImage?)
case more(UIImage?)
case avatar(Peer)
}
@ -60,6 +71,7 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
let referenceNode: ContextReferenceContentNode
let containerNode: ContextControllerSourceNode
private let iconNode: ASImageNode
private var animationNode: AnimationNode?
private let avatarNode: AvatarNode
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
@ -109,6 +121,15 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
private var content: Content?
func setContent(_ content: Content, animated: Bool = false) {
if case .more = content, self.animationNode == nil {
let iconColor = UIColor(rgb: 0xffffff)
let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor,
"Point 3.Group 1.Fill 1": iconColor,
"Point 1.Group 1.Fill 1": iconColor], scale: 1.0)
animationNode.frame = self.containerNode.bounds
self.addSubnode(animationNode)
self.animationNode = animationNode
}
if animated {
switch content {
case let .image(image):
@ -127,7 +148,12 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
self.avatarNode.setPeer(context: self.context, theme: self.theme, peer: peer)
self.iconNode.isHidden = true
self.avatarNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
self.iconNode.image = image
self.iconNode.isHidden = false
self.avatarNode.isHidden = true
self.animationNode?.isHidden = false
}
} else {
self.content = content
@ -140,6 +166,12 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
self.avatarNode.setPeer(context: self.context, theme: self.theme, peer: peer)
self.iconNode.isHidden = true
self.avatarNode.isHidden = false
self.animationNode?.isHidden = true
case let .more(image):
self.iconNode.image = image
self.iconNode.isHidden = false
self.avatarNode.isHidden = true
self.animationNode?.isHidden = false
}
}
}
@ -155,4 +187,8 @@ final class VoiceChatHeaderButton: HighlightableButtonNode {
func onLayout() {
}
func play() {
self.animationNode?.playOnce()
}
}

View File

@ -202,7 +202,6 @@ public func createGroupCall(account: Account, peerId: PeerId, title: String?, sc
guard let inputPeer = inputPeer else {
return .fail(.generic)
}
var flags: Int32 = 0
if let _ = title {
flags |= (1 << 0)
@ -1188,10 +1187,12 @@ public final class GroupCallParticipantsContext {
public final class MemberEvent {
public let peerId: PeerId
public let canUnmute: Bool
public let joined: Bool
public init(peerId: PeerId, joined: Bool) {
public init(peerId: PeerId, canUnmute: Bool, joined: Bool) {
self.peerId = peerId
self.canUnmute = canUnmute
self.joined = joined
}
}
@ -1643,7 +1644,7 @@ public final class GroupCallParticipantsContext {
if let index = updatedParticipants.firstIndex(where: { $0.peer.id == participantUpdate.peerId }) {
updatedParticipants.remove(at: index)
updatedTotalCount = max(0, updatedTotalCount - 1)
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, joined: false))
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, canUnmute: false, joined: false))
} else if isVersionUpdate {
updatedTotalCount = max(0, updatedTotalCount - 1)
}
@ -1666,7 +1667,7 @@ public final class GroupCallParticipantsContext {
updatedParticipants.remove(at: index)
} else if case .joined = participantUpdate.participationStatusChange {
updatedTotalCount += 1
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, joined: true))
strongSelf.memberEventsPipe.putNext(MemberEvent(peerId: participantUpdate.peerId, canUnmute: participantUpdate.muteState?.canUnmute ?? true, joined: true))
}
var activityTimestamp: Double?

View File

@ -0,0 +1 @@
import Foundation

View File

@ -134,7 +134,6 @@ public final class CoveredStickerSet : Equatable {
}
public func installStickerSetInteractively(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal<InstallStickerSetResult, InstallStickerSetError> {
return account.network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse)) |> mapError { _ -> InstallStickerSetError in
return .generic
} |> mapToSignal { result -> Signal<InstallStickerSetResult, InstallStickerSetError> in

View File

@ -56,6 +56,7 @@ public struct PresentationResourcesSettings {
public static let support = renderIcon(name: "Settings/MenuIcons/Support")
public static let faq = renderIcon(name: "Settings/MenuIcons/Faq")
public static let tips = renderIcon(name: "Settings/MenuIcons/Tips")
public static let addAccount = renderIcon(name: "Settings/MenuIcons/AddAccount")
public static let setPasscode = renderIcon(name: "Settings/MenuIcons/SetPasscode")

View File

@ -458,8 +458,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(titleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds))
}
} else if let duration = duration {
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
if message.author?.id.namespace == Namespaces.Peer.CloudChannel {
let titleString = strings.Notification_VoiceChatEnded(callDurationString(strings: strings, value: duration)).0
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
} else {
let attributePeerIds: [(Int, PeerId?)] = [(0, message.author?.id)]
let titleString = strings.Notification_VoiceChatEndedGroup(authorName, callDurationString(strings: strings, value: duration))
attributedString = addAttributesToStringWithRanges(titleString, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: attributePeerIds))
}
} else {
if message.author?.id.namespace == Namespaces.Peer.CloudChannel {
let titleString = strings.Notification_VoiceChatStartedChannel

View File

@ -220,6 +220,7 @@ swift_library(
"//submodules/Speak:Speak",
"//submodules/PeerInfoAvatarListNode:PeerInfoAvatarListNode",
"//submodules/DebugSettingsUI:DebugSettingsUI",
"//submodules/ImportStickerPackUI:ImportStickerPackUI",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_vc_volume.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ic_vc_camera.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,7 +1,7 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "tips_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.964706 0.768627 0.262745 scn
0.000000 18.799999 m
0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c
1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c
5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c
18.799999 30.000000 l
22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c
27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c
30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c
30.000000 11.200001 l
30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c
28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c
24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c
11.200000 0.000000 l
7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c
2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c
0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c
0.000000 18.799999 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 8.500000 5.000000 cm
1.000000 1.000000 1.000000 scn
0.000000 12.500000 m
0.000000 16.089851 2.910149 19.000000 6.500000 19.000000 c
10.089851 19.000000 13.000000 16.089851 13.000000 12.500000 c
13.000000 9.326989 11.698906 7.884006 10.621044 6.688600 c
10.292052 6.323730 9.983858 5.981926 9.739806 5.621033 c
9.164136 4.769756 8.876301 4.344118 8.752400 4.243239 c
8.663695 4.171017 8.625642 4.138249 8.582385 4.115283 c
8.539128 4.092316 8.490668 4.079148 8.381149 4.046125 c
8.228177 4.000000 7.977118 4.000000 7.475001 4.000000 c
5.525000 4.000000 l
5.022882 4.000000 4.771823 4.000000 4.618851 4.046125 c
4.509332 4.079148 4.460872 4.092316 4.417615 4.115283 c
4.374358 4.138249 4.336305 4.171017 4.247600 4.243239 c
4.123699 4.344119 3.835864 4.769756 3.260195 5.621032 c
3.016143 5.981926 2.707948 6.323730 2.378956 6.688601 c
1.301094 7.884007 0.000000 9.326990 0.000000 12.500000 c
h
5.500000 3.000000 m
4.947715 3.000000 4.500000 2.552284 4.500000 2.000000 c
4.500000 0.895432 5.395431 0.000000 6.500000 0.000000 c
7.604569 0.000000 8.500000 0.895430 8.500000 2.000000 c
8.500000 2.552284 8.052285 3.000000 7.500000 3.000000 c
5.500000 3.000000 l
h
f*
n
Q
endstream
endobj
3 0 obj
2149
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002239 00000 n
0000002262 00000 n
0000002435 00000 n
0000002509 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2568
%%EOF

View File

@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_wallet.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -949,8 +949,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case let .joinVoiceChat(peerId, invite):
strongSelf.presentController(VoiceChatJoinScreen(context: strongSelf.context, peerId: peerId, invite: invite, join: { call in
}), .window(.root), nil)
case .importStickers:
break
}
}
}))

View File

@ -23,6 +23,7 @@ import ShareController
import ChatInterfaceState
import TelegramCallsUI
import UndoUI
import ImportStickerPackUI
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
if case .default = navigation {
@ -480,5 +481,16 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
}), on: .root, blockInteraction: false, completion: {})
}))
}
case .importStickers:
dismissInput()
if let navigationController = navigationController, let data = UIPasteboard.general.data(forPasteboardType: "org.telegram.third-party.stickerpack"), let stickerPack = ImportStickerPack(data: data) {
for controller in navigationController.overlayControllers {
if controller is ImportStickerPackController {
controller.dismiss()
}
}
let controller = ImportStickerPackController(context: context, stickerPack: stickerPack, parentNavigationController: navigationController)
present(controller, nil)
}
}
}

View File

@ -678,7 +678,9 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
}
}
} else {
if parsedUrl.host == "settings" {
if parsedUrl.host == "importStickers" {
handleResolvedUrl(.importStickers)
} else if parsedUrl.host == "settings" {
if let path = parsedUrl.pathComponents.last {
var section: ResolvedUrlSettingsSection?
switch path {

View File

@ -406,6 +406,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
let immediateThumbnailData: Data?
var id: Int64
switch item {
case .custom:
representations = []
videoRepresentations = []
immediateThumbnailData = nil
id = 0
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations
videoRepresentations = videoRepresentationsValue
@ -696,6 +701,11 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
let immediateThumbnailData: Data?
var id: Int64
switch item {
case .custom:
representations = []
videoRepresentations = []
immediateThumbnailData = nil
id = 0
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations
videoRepresentations = videoRepresentationsValue

View File

@ -524,6 +524,7 @@ private enum PeerInfoSettingsSection {
case watch
case support
case faq
case tips
case phoneNumber
case username
case addAccount
@ -823,6 +824,9 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
items[.support]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_FAQ, icon: PresentationResourcesSettings.faq, action: {
interaction.openSettings(.faq)
}))
items[.support]!.append(PeerInfoScreenDisclosureItem(id: 2, text: presentationData.strings.Settings_Tips, icon: PresentationResourcesSettings.tips, action: {
interaction.openSettings(.tips)
}))
var result: [(AnyHashable, [PeerInfoScreenItem])] = []
for section in SettingsSection.allCases {
@ -1542,6 +1546,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
private let hasTwoStepAuth = Promise<Bool?>(nil)
private let hasPassport = Promise<Bool>(false)
private let supportPeerDisposable = MetaDisposable()
private let tipsPeerDisposable = MetaDisposable()
private let cachedFaq = Promise<ResolvedUrl?>(nil)
private let _ready = Promise<Bool>()
@ -2869,6 +2874,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
self.resolvePeerByNameDisposable?.dispose()
self.navigationActionDisposable.dispose()
self.enqueueMediaMessageDisposable.dispose()
self.supportPeerDisposable.dispose()
self.tipsPeerDisposable.dispose()
}
override func didLoad() {
@ -5433,6 +5440,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
})]), in: .window(.root))
case .faq:
self.openFaq()
case .tips:
self.openTips()
case .phoneNumber:
if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone {
self.controller?.push(ChangePhoneNumberIntroController(context: self.context, phoneNumber: phoneNumber))
@ -5451,7 +5460,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
private func openFaq(anchor: String? = nil) {
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil))
self.controller?.present(controller, in: .window(.root))
let _ = (self.cachedFaq.get()
|> take(1)
@ -5471,6 +5480,20 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
})
}
private func openTips() {
let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil))
self.controller?.present(controller, in: .window(.root))
let context = self.context
let navigationController = self.controller?.navigationController as? NavigationController
self.tipsPeerDisposable.set((self.context.engine.peers.resolvePeerByName(name: self.presentationData.strings.Settings_TipsUsername) |> deliverOnMainQueue).start(next: { [weak controller] peerId in
controller?.dismiss()
if let peerId = peerId, let navigationController = navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId)))
}
}))
}
fileprivate func switchToAccount(id: AccountRecordId) {
self.accountsAndPeers.set(.never())
self.context.sharedContext.switchToAccount(id: id, fromSettingsController: nil, withChatListController: nil)