import AsyncDisplayKit import AVFoundation import Display import TelegramCore import SwiftSignalKit import Postbox import TelegramPresentationData import AccountContext import ContextUI import PhotoResources import RadialStatusNode import TelegramStringFormatting import GridMessageSelectionNode import UniversalMediaPlayer import ListMessageItem import ChatMessageInteractiveMediaBadge import SparseItemGrid import ShimmerEffect import QuartzCore import DirectMediaImageCache import ComponentFlow import TelegramNotices import TelegramUIPreferences import CheckNode import AppBundle import InvisibleInkDustNode import MediaPickerUI import StoryContainerScreen import EmptyStateIndicatorComponent import UIKitRuntimeUtils import PeerInfoPaneNode import ShareController import UndoUI import PlainButtonComponent import ComponentDisplayAdapters import MediaEditorScreen import AvatarNode import LocationUI import CoreLocation import Geocoding import ItemListUI import MultilineTextComponent import LocationUI import TabSelectorComponent import LanguageSelectionScreen private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white private final class VisualMediaItemInteraction { let openItem: (EngineStoryItem) -> Void let openItemContextActions: (EngineStoryItem, ASDisplayNode, CGRect, ContextGesture?) -> Void let toggleSelection: (Int32, Bool) -> Void var hiddenStories = Set() var selectedIds: Set? init( openItem: @escaping (EngineStoryItem) -> Void, openItemContextActions: @escaping (EngineStoryItem, ASDisplayNode, CGRect, ContextGesture?) -> Void, toggleSelection: @escaping (Int32, Bool) -> Void ) { self.openItem = openItem self.openItemContextActions = openItemContextActions self.toggleSelection = toggleSelection } } private final class VisualMediaHoleAnchor: SparseItemGrid.HoleAnchor { let storyId: StoryId override var id: AnyHashable { return AnyHashable(self.storyId) } let indexValue: Int override var index: Int { return self.indexValue } let localMonthTimestamp: Int32 override var tag: Int32 { return self.localMonthTimestamp } init(index: Int, storyId: StoryId, localMonthTimestamp: Int32) { self.indexValue = index self.storyId = storyId self.localMonthTimestamp = localMonthTimestamp } } private final class VisualMediaItem: SparseItemGrid.Item { let indexValue: Int override var index: Int { return self.indexValue } override var isReorderable: Bool { return self.isReorderableValue } let localMonthTimestamp: Int32 let peer: PeerReference let storyId: StoryId let story: EngineStoryItem let authorPeer: EnginePeer? let isPinned: Bool let isReorderableValue: Bool override var id: AnyHashable { return AnyHashable(self.storyId) } override var tag: Int32 { return self.localMonthTimestamp } override var holeAnchor: SparseItemGrid.HoleAnchor { return VisualMediaHoleAnchor(index: self.index, storyId: self.storyId, localMonthTimestamp: self.localMonthTimestamp) } init(index: Int, peer: PeerReference, storyId: StoryId, story: EngineStoryItem, authorPeer: EnginePeer?, isPinned: Bool, localMonthTimestamp: Int32, isReorderable: Bool) { self.indexValue = index self.peer = peer self.storyId = storyId self.story = story self.authorPeer = authorPeer self.isPinned = isPinned self.localMonthTimestamp = localMonthTimestamp self.isReorderableValue = isReorderable } } private struct Month: Equatable { var packedValue: Int32 init(packedValue: Int32) { self.packedValue = packedValue } init(localTimestamp: Int32) { var time: time_t = time_t(localTimestamp) var timeinfo: tm = tm() gmtime_r(&time, &timeinfo) let year = UInt32(timeinfo.tm_year) let month = UInt32(timeinfo.tm_mon) self.packedValue = Int32(bitPattern: year | (month << 16)) } var year: Int32 { return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 0) & 0xffff) } var month: Int32 { return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 16) & 0xffff) } } private let durationFont: UIFont = { Font.semibold(11.0) }() private let avatarFont: UIFont = { avatarPlaceholderFont(size: 10.0) }() private let minDurationImage: UIImage = { let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) if let image = UIImage(bundleImageName: "Chat/GridPlayIcon") { UIGraphicsPushContext(context) image.draw(in: CGRect(origin: CGPoint(x: (size.width - image.size.width) / 2.0, y: (size.height - image.size.height) / 2.0), size: image.size)) UIGraphicsPopContext() } }) return image! }() private let leftShadowImage: UIImage = { let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")! let image = generateImage(baseImage.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: -1.0, y: 1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() }) return image! }() private let rightShadowImage: UIImage = { let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")! let image = generateImage(baseImage.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() }) return image! }() private let topRightShadowImage: UIImage = { let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")! let image = generateImage(baseImage.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() }) return image! }() private let topLeftShadowImage: UIImage = { let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")! let image = generateImage(baseImage.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: -1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() }) return image! }() private let viewCountImage: UIImage = { let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridViewCount")! let image = generateImage(baseImage.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() }) return image! }() private let privacyTypeImageScaleFactor: CGFloat = { return 0.9 }() private let topRightIconPinnedImage: UIImage = { let baseImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/Pinned"), color: .white)! let imageSize = CGSize(width: floor(baseImage.size.width * 1.0), height: floor(baseImage.size.width * 1.0)) let image = generateImage(CGSize(width: imageSize.width + 4.0, height: imageSize.height + 4.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: imageSize)) UIGraphicsPopContext() }) return image! }() private let privacyTypeContactsImage: UIImage = { let baseImage = UIImage(bundleImageName: "Stories/PrivacyContacts")! let imageSize = CGSize(width: floor(baseImage.size.width * privacyTypeImageScaleFactor), height: floor(baseImage.size.width * privacyTypeImageScaleFactor)) let image = generateImage(imageSize, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() }) return image! }() private let privacyTypeCloseFriendsImage: UIImage = { let baseImage = UIImage(bundleImageName: "Stories/PrivacyCloseFriends")! let imageSize = CGSize(width: floor(baseImage.size.width * privacyTypeImageScaleFactor), height: floor(baseImage.size.width * privacyTypeImageScaleFactor)) let image = generateImage(imageSize, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() }) return image! }() private let privacyTypeSelectedImage: UIImage = { let baseImage = UIImage(bundleImageName: "Stories/PrivacySelectedContacts")! let imageSize = CGSize(width: floor(baseImage.size.width * privacyTypeImageScaleFactor), height: floor(baseImage.size.width * privacyTypeImageScaleFactor)) let image = generateImage(imageSize, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) baseImage.draw(in: CGRect(origin: CGPoint(), size: size)) UIGraphicsPopContext() }) return image! }() private enum ItemTopRightIcon { case privacyContacts case privacyCloseFriends case privacySelected case pinned } private final class DurationLayer: SimpleLayer { private var authorPeerId: EnginePeer.Id? private var avatarLayer: SimpleLayer? private var disposable: Disposable? override init() { super.init() self.contentsGravity = .topRight self.contentsScale = UIScreenScale } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.disposable?.dispose() } override func action(forKey event: String) -> CAAction? { return nullAction } func update(viewCount: Int32, isMin: Bool) { if isMin { self.contents = nil } else { let countString: String if viewCount > 1000000 { countString = "\(viewCount / 1000000)M" } else if viewCount > 1000 { countString = "\(viewCount / 1000)K" } else { countString = "\(viewCount)" } let string = NSAttributedString(string: countString, font: durationFont, textColor: .white) let bounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) let textSize = CGSize(width: ceil(bounds.width), height: ceil(bounds.height)) let sideInset: CGFloat = 6.0 let verticalInset: CGFloat = 2.0 let iconSpacing: CGFloat = -3.0 let image = generateImage(CGSize(width: viewCountImage.size.width + iconSpacing + textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.normal) context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor) UIGraphicsPushContext(context) viewCountImage.draw(in: CGRect(origin: CGPoint(x: 0.0, y: (size.height - viewCountImage.size.height) * 0.5), size: viewCountImage.size)) string.draw(in: bounds.offsetBy(dx: sideInset + viewCountImage.size.width + iconSpacing, dy: verticalInset)) UIGraphicsPopContext() }) self.contents = image?.cgImage } } func update(duration: Int32, isMin: Bool) { if isMin { self.contents = minDurationImage.cgImage } else { let string = NSAttributedString(string: stringForDuration(duration), font: durationFont, textColor: .white) let bounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) let textSize = CGSize(width: ceil(bounds.width), height: ceil(bounds.height)) let sideInset: CGFloat = 6.0 let verticalInset: CGFloat = 2.0 let image = generateImage(CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.normal) context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor) UIGraphicsPushContext(context) string.draw(in: bounds.offsetBy(dx: sideInset, dy: verticalInset)) UIGraphicsPopContext() }) self.contents = image?.cgImage } } func update(topRightIcon: ItemTopRightIcon, isMin: Bool) { if isMin { self.contents = nil } else { let iconImage: UIImage switch topRightIcon { case .pinned: iconImage = topRightIconPinnedImage case .privacyContacts: iconImage = privacyTypeContactsImage case .privacyCloseFriends: iconImage = privacyTypeCloseFriendsImage case .privacySelected: iconImage = privacyTypeSelectedImage } let sideInset: CGFloat = 0.0 let verticalInset: CGFloat = 0.0 let image = generateImage(CGSize(width: iconImage.size.width + sideInset * 2.0, height: iconImage.size.height + verticalInset * 2.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.normal) context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor) UIGraphicsPushContext(context) iconImage.draw(in: CGRect(origin: CGPoint(x: (size.width - iconImage.size.width) * 0.5, y: (size.height - iconImage.size.height) * 0.5), size: iconImage.size)) UIGraphicsPopContext() }) self.contents = image?.cgImage } } func copyAuthor(from other: DurationLayer) { self.contents = other.contents let avatarLayer: SimpleLayer if let current = self.avatarLayer { avatarLayer = current } else { avatarLayer = SimpleLayer() self.avatarLayer = avatarLayer self.addSublayer(avatarLayer) avatarLayer.frame = CGRect(origin: CGPoint(x: -11.0, y: 2.0), size: CGSize(width: 13.0, height: 13.0)) avatarLayer.cornerRadius = 13.0 * 0.5 avatarLayer.masksToBounds = true } avatarLayer.contents = other.avatarLayer?.contents } func update(directMediaImageCache: DirectMediaImageCache, author: EnginePeer, constrainedWidth: CGFloat, synchronous: SparseItemGrid.Synchronous) { let avatarLayer: SimpleLayer if let current = self.avatarLayer { avatarLayer = current } else { avatarLayer = SimpleLayer() self.avatarLayer = avatarLayer self.addSublayer(avatarLayer) avatarLayer.frame = CGRect(origin: CGPoint(x: -11.0, y: 2.0), size: CGSize(width: 13.0, height: 13.0)) avatarLayer.cornerRadius = 13.0 * 0.5 avatarLayer.masksToBounds = true } if self.authorPeerId != author.id { let string = NSAttributedString(string: author.debugDisplayTitle, font: durationFont, textColor: .white) let bounds = string.boundingRect(with: CGSize(width: constrainedWidth - 24.0, height: 20.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil) let textSize = CGSize(width: ceil(bounds.width), height: ceil(bounds.height)) let sideInset: CGFloat = 6.0 let verticalInset: CGFloat = 2.0 let image = generateImage(CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setBlendMode(.normal) context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor) UIGraphicsPushContext(context) string.draw(with: bounds.offsetBy(dx: sideInset, dy: verticalInset), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil) UIGraphicsPopContext() }) self.contents = image?.cgImage if let smallProfileImage = author.smallProfileImage, let peerReference = PeerReference(author._asPeer()) { if let result = directMediaImageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: smallProfileImage.immediateThumbnailData, size: 24, includeBlurred: true, synchronous: synchronous == .full) { if let image = result.image { avatarLayer.contents = image.cgImage } else if let image = result.blurredImage { avatarLayer.contents = image.cgImage } if let loadSignal = result.loadSignal { self.disposable?.dispose() self.disposable = (loadSignal |> deliverOnMainQueue).start(next: { [weak self] image in guard let self else { return } self.avatarLayer?.contents = image?.cgImage }) } } } else { self.avatarLayer?.contents = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: author.displayLetters, peerId: author.id, nameColor: author.nameColor) })?.cgImage } } } } private final class ItemLayer: CALayer, SparseItemGridLayer { struct Params: Equatable { let size: CGSize let viewCount: Int32? let duration: Int32? let topRightIcon: ItemTopRightIcon? let authorId: EnginePeer.Id? let isMin: Bool let minFactor: CGFloat init(size: CGSize, viewCount: Int32?, duration: Int32?, topRightIcon: ItemTopRightIcon?, authorId: EnginePeer.Id?, isMin: Bool, minFactor: CGFloat) { self.size = size self.viewCount = viewCount self.duration = duration self.topRightIcon = topRightIcon self.authorId = authorId self.isMin = isMin self.minFactor = minFactor } } var item: VisualMediaItem? var viewCountLayer: DurationLayer? var durationLayer: DurationLayer? var privacyTypeLayer: DurationLayer? var authorLayer: DurationLayer? var leftShadowLayer: SimpleLayer? var rightShadowLayer: SimpleLayer? var topRightShadowLayer: SimpleLayer? var topLeftShadowLayer: SimpleLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? var dustLayer: MediaDustLayer? var disposable: Disposable? var currentParams: Params? var hasContents: Bool = false override init() { super.init() self.contentsGravity = .resize } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override init(layer: Any) { super.init(layer: layer) } deinit { self.disposable?.dispose() } func getContents() -> Any? { return self.contents } func setContents(_ contents: Any?) { if let image = contents as? UIImage { self.contents = image.cgImage } } func setSpoilerContents(_ contents: Any?) { if let image = contents as? UIImage { self.dustLayer?.contents = image.cgImage } } override func action(forKey event: String) -> CAAction? { return nullAction } func bind(item: VisualMediaItem) { self.item = item } func updateDuration(size: CGSize, viewCount: Int32?, duration: Int32?, topRightIcon: ItemTopRightIcon?, author: EnginePeer?, isMin: Bool, minFactor: CGFloat, directMediaImageCache: DirectMediaImageCache, synchronous: SparseItemGrid.Synchronous) { let params = Params(size: size, viewCount: viewCount, duration: duration, topRightIcon: topRightIcon, authorId: author?.id, isMin: isMin, minFactor: minFactor) if self.currentParams == params { return } self.currentParams = params self.minFactor = minFactor if let viewCount { if let viewCountLayer = self.viewCountLayer { viewCountLayer.update(viewCount: viewCount, isMin: isMin) } else { let viewCountLayer = DurationLayer() viewCountLayer.contentsGravity = .topLeft viewCountLayer.update(viewCount: viewCount, isMin: isMin) self.addSublayer(viewCountLayer) viewCountLayer.frame = CGRect(origin: CGPoint(x: 7.0, y: self.bounds.height - 4.0), size: CGSize()) viewCountLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0) self.viewCountLayer = viewCountLayer } } else if let viewCountLayer = self.viewCountLayer { self.viewCountLayer = nil viewCountLayer.removeFromSuperlayer() } if let duration { if let durationLayer = self.durationLayer { durationLayer.update(duration: duration, isMin: isMin) } else { let durationLayer = DurationLayer() durationLayer.update(duration: duration, isMin: isMin) self.addSublayer(durationLayer) durationLayer.frame = CGRect(origin: CGPoint(x: self.bounds.width - 3.0, y: self.bounds.height - 4.0), size: CGSize()) durationLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0) self.durationLayer = durationLayer } } else if let durationLayer = self.durationLayer { self.durationLayer = nil durationLayer.removeFromSuperlayer() } if let topRightIcon { if let privacyTypeLayer = self.privacyTypeLayer { privacyTypeLayer.update(topRightIcon: topRightIcon, isMin: isMin) } else { let privacyTypeLayer = DurationLayer() privacyTypeLayer.contentsGravity = .bottomRight privacyTypeLayer.update(topRightIcon: topRightIcon, isMin: isMin) self.addSublayer(privacyTypeLayer) privacyTypeLayer.frame = CGRect(origin: CGPoint(x: self.bounds.width - 2.0, y: 3.0), size: CGSize()) privacyTypeLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0) self.privacyTypeLayer = privacyTypeLayer } } else if let privacyTypeLayer = self.privacyTypeLayer { self.privacyTypeLayer = nil privacyTypeLayer.removeFromSuperlayer() } if let author { if let authorLayer = self.authorLayer { authorLayer.update(directMediaImageCache: directMediaImageCache, author: author, constrainedWidth: size.width, synchronous: synchronous) } else { let authorLayer = DurationLayer() authorLayer.contentsGravity = .bottomLeft authorLayer.update(directMediaImageCache: directMediaImageCache, author: author, constrainedWidth: size.width, synchronous: synchronous) self.addSublayer(authorLayer) authorLayer.frame = CGRect(origin: CGPoint(x: 17.0, y: 3.0), size: CGSize()) authorLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0) self.authorLayer = authorLayer } } else if let authorLayer = self.authorLayer { self.authorLayer = nil authorLayer.removeFromSuperlayer() } let size = self.bounds.size if self.viewCountLayer != nil { if self.leftShadowLayer == nil { let leftShadowLayer = SimpleLayer() self.leftShadowLayer = leftShadowLayer self.insertSublayer(leftShadowLayer, at: 0) leftShadowLayer.contents = leftShadowImage.cgImage let shadowSize = CGSize(width: min(size.width, leftShadowImage.size.width), height: min(size.height, leftShadowImage.size.height)) leftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - shadowSize.height), size: shadowSize) } } else { if let leftShadowLayer = self.leftShadowLayer { self.leftShadowLayer = nil leftShadowLayer.removeFromSuperlayer() } } if self.durationLayer != nil { if self.rightShadowLayer == nil { let rightShadowLayer = SimpleLayer() self.rightShadowLayer = rightShadowLayer self.insertSublayer(rightShadowLayer, at: 0) rightShadowLayer.contents = rightShadowImage.cgImage let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height)) rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize) } } else { if let rightShadowLayer = self.rightShadowLayer { self.rightShadowLayer = nil rightShadowLayer.removeFromSuperlayer() } } if self.privacyTypeLayer != nil { if self.topRightShadowLayer == nil { let topRightShadowLayer = SimpleLayer() self.topRightShadowLayer = topRightShadowLayer self.insertSublayer(topRightShadowLayer, at: 0) topRightShadowLayer.contents = topRightShadowImage.cgImage let shadowSize = CGSize(width: min(size.width, topRightShadowImage.size.width), height: min(size.height, topRightShadowImage.size.height)) topRightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: 0.0), size: shadowSize) } } else { if let topRightShadowLayer = self.topRightShadowLayer { self.topRightShadowLayer = nil topRightShadowLayer.removeFromSuperlayer() } } if self.authorLayer != nil { if self.topLeftShadowLayer == nil { let topLeftShadowLayer = SimpleLayer() self.topLeftShadowLayer = topLeftShadowLayer self.insertSublayer(topLeftShadowLayer, at: 0) topLeftShadowLayer.contents = topLeftShadowImage.cgImage let shadowSize = CGSize(width: min(size.width, topLeftShadowImage.size.width), height: min(size.height, topLeftShadowImage.size.height)) topLeftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: shadowSize) } } else { if let topLeftShadowLayer = self.topLeftShadowLayer { self.topLeftShadowLayer = nil topLeftShadowLayer.removeFromSuperlayer() } } } func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) { if let isSelected = isSelected { if let selectionLayer = self.selectionLayer { selectionLayer.updateSelected(isSelected, animated: animated) } else { let selectionLayer = GridMessageSelectionLayer(theme: theme) selectionLayer.updateSelected(isSelected, animated: false) self.selectionLayer = selectionLayer self.addSublayer(selectionLayer) if !self.bounds.isEmpty { selectionLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size) selectionLayer.updateLayout(size: self.bounds.size) if animated { selectionLayer.animateIn() } } } } else if let selectionLayer = self.selectionLayer { self.selectionLayer = nil if animated { selectionLayer.animateOut { [weak selectionLayer] in selectionLayer?.removeFromSuperlayer() } } else { selectionLayer.removeFromSuperlayer() } } if let privacyTypeLayer = self.privacyTypeLayer { let privacyAlpha: Float = isSelected == nil ? 1.0 : 0.0 if privacyAlpha != privacyTypeLayer.opacity { let previousAlpha = privacyTypeLayer.opacity privacyTypeLayer.opacity = privacyAlpha privacyTypeLayer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(privacyAlpha), duration: 0.2) } } if let authorLayer = self.authorLayer { let authorAlpha: Float = isSelected == nil ? 1.0 : 0.0 if authorAlpha != authorLayer.opacity { let previousAlpha = authorLayer.opacity authorLayer.opacity = authorAlpha authorLayer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(authorAlpha), duration: 0.2) } } } func updateHasSpoiler(hasSpoiler: Bool) { if hasSpoiler { if let _ = self.dustLayer { } else { let dustLayer = MediaDustLayer() self.dustLayer = dustLayer self.addSublayer(dustLayer) if !self.bounds.isEmpty { dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size) dustLayer.updateLayout(size: self.bounds.size) } } } else if let dustLayer = self.dustLayer { self.dustLayer = nil dustLayer.removeFromSuperlayer() } } func unbind() { self.item = nil } func needsShimmer() -> Bool { return !self.hasContents } func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { if let viewCountLayer = self.viewCountLayer { viewCountLayer.frame = CGRect(origin: CGPoint(x: 7.0, y: size.height - 4.0), size: CGSize()) } if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 4.0), size: CGSize()) } if let privacyTypeLayer = self.privacyTypeLayer { privacyTypeLayer.frame = CGRect(origin: CGPoint(x: size.width - 2.0, y: 3.0), size: CGSize()) } if let authorLayer = self.authorLayer { authorLayer.frame = CGRect(origin: CGPoint(x: 17.0, y: 3.0), size: CGSize()) } if let leftShadowLayer = self.leftShadowLayer { let shadowSize = CGSize(width: min(size.width, leftShadowImage.size.width), height: min(size.height, leftShadowImage.size.height)) leftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - shadowSize.height), size: shadowSize) } if let rightShadowLayer = self.rightShadowLayer { let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height)) rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize) } if let topRightShadowLayer = self.topRightShadowLayer { let shadowSize = CGSize(width: min(size.width, topRightShadowImage.size.width), height: min(size.height, topRightShadowImage.size.height)) topRightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: 0.0), size: shadowSize) } if let topLeftShadowLayer = self.topLeftShadowLayer { let shadowSize = CGSize(width: min(size.width, topLeftShadowImage.size.width), height: min(size.height, topLeftShadowImage.size.height)) topLeftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: shadowSize) } if let binding = binding as? SparseItemGridBindingImpl, let item = item as? VisualMediaItem, let previousItem = self.item { if previousItem.story.media.id != item.story.media.id { binding.bindLayers(items: [item], layers: [displayItem], size: size, insets: insets, synchronous: .none) } else { if let layer = displayItem.layer as? ItemLayer { var selectedMedia: Media? if let image = item.story.media._asMedia() as? TelegramMediaImage { selectedMedia = image } else if let file = item.story.media._asMedia() as? TelegramMediaFile { selectedMedia = file } if let selectedMedia { binding.updateLayerData(story: item.story, item: item, selectedMedia: selectedMedia, layer: layer, synchronous: .none) } } } } } } private final class ItemTransitionView: UIView { private weak var itemLayer: CALayer? private var copyDurationLayer: SimpleLayer? private var copyViewCountLayer: SimpleLayer? private var copyPrivacyTypeLayer: SimpleLayer? private var copyAuthorLayer: SimpleLayer? private var copyLeftShadowLayer: SimpleLayer? private var copyRightShadowLayer: SimpleLayer? private var copyTopRightShadowLayer: SimpleLayer? private var copyTopLeftShadowLayer: SimpleLayer? private var viewCountLayerBottomLeftPosition: CGPoint? private var durationLayerBottomLeftPosition: CGPoint? private var privacyTypeLayerTopRightPosition: CGPoint? private var authorLayerTopLeftPosition: CGPoint? var selectionLayer: GridMessageSelectionLayer? init(itemLayer: CALayer?) { self.itemLayer = itemLayer super.init(frame: CGRect()) if let itemLayer { self.layer.contentsRect = itemLayer.contentsRect var viewCountLayer: CALayer? var durationLayer: CALayer? var privacyTypeLayer: CALayer? var authorLayer: CALayer? var leftShadowLayer: CALayer? var rightShadowLayer: CALayer? var topRightShadowLayer: CALayer? var topLeftShadowLayer: CALayer? if let itemLayer = itemLayer as? ItemLayer { viewCountLayer = itemLayer.viewCountLayer durationLayer = itemLayer.durationLayer privacyTypeLayer = itemLayer.privacyTypeLayer authorLayer = itemLayer.authorLayer leftShadowLayer = itemLayer.leftShadowLayer rightShadowLayer = itemLayer.rightShadowLayer topRightShadowLayer = itemLayer.topRightShadowLayer topLeftShadowLayer = itemLayer.topLeftShadowLayer self.layer.contents = itemLayer.contents } if let leftShadowLayer { let copyLayer = SimpleLayer() copyLayer.contents = leftShadowLayer.contents copyLayer.contentsRect = leftShadowLayer.contentsRect copyLayer.contentsGravity = leftShadowLayer.contentsGravity copyLayer.contentsScale = leftShadowLayer.contentsScale copyLayer.frame = leftShadowLayer.frame self.layer.addSublayer(copyLayer) self.copyLeftShadowLayer = copyLayer } if let rightShadowLayer { let copyLayer = SimpleLayer() copyLayer.contents = rightShadowLayer.contents copyLayer.contentsRect = rightShadowLayer.contentsRect copyLayer.contentsGravity = rightShadowLayer.contentsGravity copyLayer.contentsScale = rightShadowLayer.contentsScale copyLayer.frame = rightShadowLayer.frame self.layer.addSublayer(copyLayer) self.copyRightShadowLayer = copyLayer } if let topRightShadowLayer { let copyLayer = SimpleLayer() copyLayer.contents = topRightShadowLayer.contents copyLayer.contentsRect = topRightShadowLayer.contentsRect copyLayer.contentsGravity = topRightShadowLayer.contentsGravity copyLayer.contentsScale = topRightShadowLayer.contentsScale copyLayer.frame = topRightShadowLayer.frame self.layer.addSublayer(copyLayer) self.copyTopRightShadowLayer = copyLayer } if let topLeftShadowLayer { let copyLayer = SimpleLayer() copyLayer.contents = topLeftShadowLayer.contents copyLayer.contentsRect = topLeftShadowLayer.contentsRect copyLayer.contentsGravity = topLeftShadowLayer.contentsGravity copyLayer.contentsScale = topLeftShadowLayer.contentsScale copyLayer.frame = topLeftShadowLayer.frame self.layer.addSublayer(copyLayer) self.copyTopLeftShadowLayer = copyLayer } if let viewCountLayer { let copyViewCountLayer = SimpleLayer() copyViewCountLayer.contents = viewCountLayer.contents copyViewCountLayer.contentsRect = viewCountLayer.contentsRect copyViewCountLayer.contentsGravity = viewCountLayer.contentsGravity copyViewCountLayer.contentsScale = viewCountLayer.contentsScale copyViewCountLayer.frame = viewCountLayer.frame self.layer.addSublayer(copyViewCountLayer) self.copyViewCountLayer = copyViewCountLayer self.viewCountLayerBottomLeftPosition = CGPoint(x: viewCountLayer.frame.minX, y: itemLayer.bounds.height - viewCountLayer.frame.maxY) } if let privacyTypeLayer { let copyPrivacyTypeLayer = SimpleLayer() copyPrivacyTypeLayer.contents = privacyTypeLayer.contents copyPrivacyTypeLayer.contentsRect = privacyTypeLayer.contentsRect copyPrivacyTypeLayer.contentsGravity = privacyTypeLayer.contentsGravity copyPrivacyTypeLayer.contentsScale = privacyTypeLayer.contentsScale copyPrivacyTypeLayer.frame = privacyTypeLayer.frame self.layer.addSublayer(copyPrivacyTypeLayer) self.copyPrivacyTypeLayer = copyPrivacyTypeLayer self.privacyTypeLayerTopRightPosition = CGPoint(x: itemLayer.bounds.width - privacyTypeLayer.frame.maxX, y: privacyTypeLayer.frame.minY) } if let authorLayer = authorLayer as? DurationLayer { let copyAuthorLayer = DurationLayer() copyAuthorLayer.contentsRect = authorLayer.contentsRect copyAuthorLayer.contentsGravity = authorLayer.contentsGravity copyAuthorLayer.contentsScale = authorLayer.contentsScale copyAuthorLayer.frame = authorLayer.frame copyAuthorLayer.copyAuthor(from: authorLayer) self.layer.addSublayer(copyAuthorLayer) self.copyAuthorLayer = copyAuthorLayer self.authorLayerTopLeftPosition = CGPoint(x: authorLayer.frame.minX, y: authorLayer.frame.minY) } if let durationLayer { let copyDurationLayer = SimpleLayer() copyDurationLayer.contents = durationLayer.contents copyDurationLayer.contentsRect = durationLayer.contentsRect copyDurationLayer.contentsGravity = durationLayer.contentsGravity copyDurationLayer.contentsScale = durationLayer.contentsScale copyDurationLayer.frame = durationLayer.frame self.layer.addSublayer(copyDurationLayer) self.copyDurationLayer = copyDurationLayer self.durationLayerBottomLeftPosition = CGPoint(x: itemLayer.bounds.width - durationLayer.frame.maxX, y: itemLayer.bounds.height - durationLayer.frame.maxY) } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(state: StoryContainerScreen.TransitionState, transition: ComponentTransition) { let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) if let copyDurationLayer = self.copyDurationLayer, let durationLayerBottomLeftPosition = self.durationLayerBottomLeftPosition { transition.setFrame(layer: copyDurationLayer, frame: CGRect(origin: CGPoint(x: size.width - durationLayerBottomLeftPosition.x - copyDurationLayer.bounds.width, y: size.height - durationLayerBottomLeftPosition.y - copyDurationLayer.bounds.height), size: copyDurationLayer.bounds.size)) } if let copyViewCountLayer = self.copyViewCountLayer, let viewCountLayerBottomLeftPosition = self.viewCountLayerBottomLeftPosition { transition.setFrame(layer: copyViewCountLayer, frame: CGRect(origin: CGPoint(x: viewCountLayerBottomLeftPosition.x, y: size.height - viewCountLayerBottomLeftPosition.y - copyViewCountLayer.bounds.height), size: copyViewCountLayer.bounds.size)) } if let privacyTypeLayer = self.copyPrivacyTypeLayer, let privacyTypeLayerTopRightPosition = self.privacyTypeLayerTopRightPosition { transition.setFrame(layer: privacyTypeLayer, frame: CGRect(origin: CGPoint(x: size.width - privacyTypeLayerTopRightPosition.x, y: privacyTypeLayerTopRightPosition.y), size: privacyTypeLayer.bounds.size)) } if let authorLayer = self.copyAuthorLayer, let authorLayerTopLeftPosition = self.authorLayerTopLeftPosition { transition.setFrame(layer: authorLayer, frame: CGRect(origin: CGPoint(x: authorLayerTopLeftPosition.x, y: authorLayerTopLeftPosition.y), size: authorLayer.bounds.size)) } if let copyLeftShadowLayer = self.copyLeftShadowLayer { transition.setFrame(layer: copyLeftShadowLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - copyLeftShadowLayer.bounds.height), size: copyLeftShadowLayer.bounds.size)) } if let copyRightShadowLayer = self.copyRightShadowLayer { transition.setFrame(layer: copyRightShadowLayer, frame: CGRect(origin: CGPoint(x: size.width - copyRightShadowLayer.bounds.width, y: size.height - copyRightShadowLayer.bounds.height), size: copyRightShadowLayer.bounds.size)) } if let copyTopRightShadowLayer = self.copyTopRightShadowLayer { transition.setFrame(layer: copyTopRightShadowLayer, frame: CGRect(origin: CGPoint(x: size.width - copyTopRightShadowLayer.bounds.width, y: 0.0), size: copyTopRightShadowLayer.bounds.size)) } if let copyTopLeftShadowLayer = self.copyTopLeftShadowLayer { transition.setFrame(layer: copyTopLeftShadowLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: copyTopLeftShadowLayer.bounds.size)) } } func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) { if let isSelected = isSelected { if let selectionLayer = self.selectionLayer { selectionLayer.updateSelected(isSelected, animated: animated) } else { let selectionLayer = GridMessageSelectionLayer(theme: theme) selectionLayer.updateSelected(isSelected, animated: false) self.selectionLayer = selectionLayer self.layer.addSublayer(selectionLayer) if !self.bounds.isEmpty { selectionLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size) selectionLayer.updateLayout(size: self.bounds.size) if animated { selectionLayer.animateIn() } } } } else if let selectionLayer = self.selectionLayer { self.selectionLayer = nil if animated { selectionLayer.animateOut { [weak selectionLayer] in selectionLayer?.removeFromSuperlayer() } } else { selectionLayer.removeFromSuperlayer() } } if let copyPrivacyTypeLayer = self.copyPrivacyTypeLayer { let privacyAlpha: Float = isSelected == nil ? 1.0 : 0.0 if privacyAlpha != copyPrivacyTypeLayer.opacity { let previousAlpha = copyPrivacyTypeLayer.opacity copyPrivacyTypeLayer.opacity = privacyAlpha copyPrivacyTypeLayer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(privacyAlpha), duration: 0.2) } } if let copyAuthorLayer = self.copyAuthorLayer { let privacyAlpha: Float = isSelected == nil ? 1.0 : 0.0 if privacyAlpha != copyAuthorLayer.opacity { let previousAlpha = copyAuthorLayer.opacity copyAuthorLayer.opacity = privacyAlpha copyAuthorLayer.animateAlpha(from: CGFloat(previousAlpha), to: CGFloat(privacyAlpha), duration: 0.2) } } } } private final class SparseItemGridBindingImpl: SparseItemGridBinding { let context: AccountContext let directMediaImageCache: DirectMediaImageCache let captureProtected: Bool let displayPrivacy: Bool var strings: PresentationStrings var chatPresentationData: ChatPresentationData var checkNodeTheme: CheckNodeTheme var itemInteraction: VisualMediaItemInteraction? var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal)? var onTapImpl: ((VisualMediaItem, CALayer, CGPoint) -> Void)? var onTagTapImpl: (() -> Void)? var didScrollImpl: (() -> Void)? var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? var scrollingOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)? var onBeginFastScrollingImpl: (() -> Void)? var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)? var reorderIfPossibleImpl: ((SparseItemGrid.Item, Int) -> Void)? var revealedSpoilerMessageIds = Set() private var shimmerImages: [CGFloat: UIImage] = [:] init(context: AccountContext, directMediaImageCache: DirectMediaImageCache, captureProtected: Bool, displayPrivacy: Bool) { self.context = context self.directMediaImageCache = directMediaImageCache self.captureProtected = false self.displayPrivacy = displayPrivacy let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.strings = presentationData.strings let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper) self.chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: 1.0) self.checkNodeTheme = CheckNodeTheme(theme: presentationData.theme, style: .overlay, hasInset: true) } func updatePresentationData(presentationData: PresentationData) { self.strings = presentationData.strings let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper) self.chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: 1.0) self.checkNodeTheme = CheckNodeTheme(theme: presentationData.theme, style: .overlay, hasInset: true) } func getSeparatorColor() -> UIColor { return self.chatPresentationData.theme.theme.list.itemPlainSeparatorColor } func createLayer(item: SparseItemGrid.Item) -> SparseItemGridLayer? { if let item = item as? VisualMediaItem, item.story.isForwardingDisabled { let layer = ItemLayer() setLayerDisableScreenshots(layer, true) return layer } else { return ItemLayer() } } func createView() -> SparseItemGridView? { return nil } func createShimmerLayer() -> SparseItemGridShimmerLayer? { return nil } private static let widthSpecs: ([Int], [Int]) = { let list: [(Int, Int)] = [ (50, 64), (100, 150), (140, 200), (Int.max, 280) ] return (list.map(\.0), list.map(\.1)) }() func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridDisplayItem], size: CGSize, insets: UIEdgeInsets, synchronous: SparseItemGrid.Synchronous) { for i in 0 ..< items.count { guard let item = items[i] as? VisualMediaItem else { continue } let displayItem = layers[i] guard let layer = displayItem.layer as? ItemLayer else { continue } if layer.bounds.isEmpty { continue } var imageWidthSpec: Int = SparseItemGridBindingImpl.widthSpecs.1[0] for i in 0 ..< SparseItemGridBindingImpl.widthSpecs.0.count { if Int(layer.bounds.width) <= SparseItemGridBindingImpl.widthSpecs.0[i] { imageWidthSpec = SparseItemGridBindingImpl.widthSpecs.1[i] break } } let story = item.story let hasSpoiler = false layer.updateHasSpoiler(hasSpoiler: hasSpoiler) var selectedMedia: Media? if let image = story.media._asMedia() as? TelegramMediaImage { selectedMedia = image } else if let file = story.media._asMedia() as? TelegramMediaFile { selectedMedia = file } if let selectedMedia = selectedMedia { if let result = directMediaImageCache.getImage(peer: item.peer, story: story, media: selectedMedia, width: imageWidthSpec, aspectRatio: 0.81, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, includeBlurred: hasSpoiler || displayItem.blurLayer != nil, synchronous: synchronous == .full) { if let image = result.image { layer.setContents(image) /*switch synchronous { case .none: layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self, weak layer, weak displayItem] _ in layer?.hasContents = true if let displayItem = displayItem { self?.updateShimmerLayersImpl?(displayItem) } }) default: layer.hasContents = true }*/ layer.hasContents = true } if let image = result.blurredImage { layer.setSpoilerContents(image) if let blurLayer = displayItem.blurLayer { blurLayer.contentsGravity = .resizeAspectFill blurLayer.contents = result.blurredImage?.cgImage } } if let loadSignal = result.loadSignal { layer.disposable?.dispose() let startTimestamp = CFAbsoluteTimeGetCurrent() layer.disposable = (loadSignal |> deliverOnMainQueue).start(next: { [weak self, weak layer, weak displayItem] image in guard let layer = layer else { return } let deltaTime = CFAbsoluteTimeGetCurrent() - startTimestamp var synchronousValue: Bool switch synchronous { case .none, .full: synchronousValue = false case .semi: synchronousValue = deltaTime < 0.1 } if "".isEmpty { synchronousValue = true } if let contents = layer.getContents(), !synchronousValue { let copyLayer = ItemLayer() copyLayer.contents = contents copyLayer.contentsRect = layer.contentsRect copyLayer.frame = layer.bounds if let durationLayer = layer.durationLayer { layer.insertSublayer(copyLayer, below: durationLayer) } else { layer.addSublayer(copyLayer) } copyLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyLayer] _ in copyLayer?.removeFromSuperlayer() }) layer.setContents(image) layer.hasContents = true if let displayItem = displayItem { self?.updateShimmerLayersImpl?(displayItem) } } else { layer.setContents(image) if !synchronousValue { layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak layer] _ in layer?.hasContents = true if let displayItem = displayItem { self?.updateShimmerLayersImpl?(displayItem) } }) } else { layer.hasContents = true if let displayItem = displayItem { self?.updateShimmerLayersImpl?(displayItem) } } } if let displayItem, let blurLayer = displayItem.blurLayer { blurLayer.contentsGravity = .resizeAspectFill blurLayer.contents = result.blurredImage?.cgImage } }) } } self.updateLayerData(story: story, item: item, selectedMedia: selectedMedia, layer: layer, synchronous: synchronous) } var isSelected: Bool? if let selectedIds = self.itemInteraction?.selectedIds { isSelected = selectedIds.contains(story.id) } layer.updateSelection(theme: self.checkNodeTheme, isSelected: isSelected, animated: false) layer.bind(item: item) } } func updateLayerData(story: EngineStoryItem, item: VisualMediaItem, selectedMedia: Media, layer: ItemLayer, synchronous: SparseItemGrid.Synchronous) { var viewCount: Int32? if let value = story.views?.seenCount { viewCount = Int32(value) } var topRightIcon: ItemTopRightIcon? if item.isPinned { topRightIcon = .pinned } else if self.displayPrivacy, let value = story.privacy { switch value.base { case .everyone: break case .contacts: topRightIcon = .privacyContacts case .closeFriends: topRightIcon = .privacyCloseFriends case .nobody: topRightIcon = .privacySelected } } var duration: Int32? var isMin: Bool = false if let file = selectedMedia as? TelegramMediaFile, !file.isAnimated { if let durationValue = file.duration { duration = Int32(durationValue) } isMin = layer.bounds.width < 80.0 } layer.updateDuration(size: layer.bounds.size, viewCount: viewCount, duration: duration, topRightIcon: topRightIcon, author: item.authorPeer, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0), directMediaImageCache: self.directMediaImageCache, synchronous: synchronous) } func unbindLayer(layer: SparseItemGridLayer) { guard let layer = layer as? ItemLayer else { return } layer.unbind() } func scrollerTextForTag(tag: Int32) -> String? { let month = Month(packedValue: tag) return stringForMonth(strings: self.strings, month: month.month, ofYear: month.year) } func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { if let loadHoleImpl = self.loadHoleImpl { return loadHoleImpl(anchor, location) } else { return .never() } } func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { if let reorderIfPossibleImpl = self.reorderIfPossibleImpl { reorderIfPossibleImpl(item, toIndex) } } func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) { guard let item = item as? VisualMediaItem else { return } self.onTapImpl?(item, itemLayer, point) } func onTagTap() { self.onTagTapImpl?() } func didScroll() { self.didScrollImpl?() } func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) { self.coveringInsetOffsetUpdatedImpl?(transition) } func scrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { self.scrollingOffsetUpdatedImpl?(transition) } func onBeginFastScrolling() { self.onBeginFastScrollingImpl?() } func getShimmerColors() -> SparseItemGrid.ShimmerColors { if let getShimmerColorsImpl = self.getShimmerColorsImpl { return getShimmerColorsImpl() } else { return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff) } } } private final class StorySearchHeaderComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let count: Int init( theme: PresentationTheme, strings: PresentationStrings, count: Int ) { self.theme = theme self.strings = strings self.count = count } static func ==(lhs: StorySearchHeaderComponent, rhs: StorySearchHeaderComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings != rhs.strings { return false } if lhs.count != rhs.count { return false } return true } final class View: UIView { private let title = ComponentView() private var component: StorySearchHeaderComponent? override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: StorySearchHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { if self.component?.theme !== component.theme { self.backgroundColor = component.theme.chatList.sectionHeaderFillColor } let insets = UIEdgeInsets(top: 7.0, left: 16.0, bottom: 7.0, right: 16.0) let titleString = component.strings.StoryList_GridHeaderLocationSearch(Int32(component.count)) let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: titleString, font: Font.regular(13.0), textColor: component.theme.chatList.sectionHeaderTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - insets.left - insets.right, height: 100.0) ) if let titleView = self.title.view { if titleView.superview == nil { self.addSubview(titleView) } titleView.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: titleSize) } return CGSize(width: availableSize.width, height: titleSize.height + insets.top + insets.bottom) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { public enum Scope { case peer(id: EnginePeer.Id, isSaved: Bool, isArchived: Bool) case search(peerId: EnginePeer.Id?, query: String) case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue) case botPreview(id: EnginePeer.Id) } public struct ZoomLevel { fileprivate var value: SparseItemGrid.ZoomLevel init(_ value: SparseItemGrid.ZoomLevel) { self.value = value } var rawValue: Int32 { return Int32(self.value.rawValue) } public init(rawValue: Int32) { self.value = SparseItemGrid.ZoomLevel(rawValue: Int(rawValue)) } } private struct MapInfoData: Equatable { var location: TelegramMediaMap var address: String? var distance: Double? var drivingTime: ExpectedTravelTime var transitTime: ExpectedTravelTime var walkingTime: ExpectedTravelTime var hasEta: Bool init( location: TelegramMediaMap, address: String?, distance: Double?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, hasEta: Bool ) { self.location = location self.address = address self.distance = distance self.drivingTime = drivingTime self.transitTime = transitTime self.walkingTime = walkingTime self.hasEta = hasEta } } private let context: AccountContext private let scope: Scope private let isProfileEmbedded: Bool private let canManageStories: Bool private let navigationController: () -> NavigationController? public weak var parentController: ViewController? private let contextGestureContainerNode: ContextControllerSourceNode private var mapOptionsNode: LocationOptionsNode? private var mapNode: LocationMapHeaderNode? private var mapDisposable: Disposable? private var locationViewState: LocationViewState = LocationViewState() { didSet { self.locationViewStatePromise.set(.single(self.locationViewState)) } } private let locationViewStatePromise = Promise(LocationViewState()) private var mapInfoData: MapInfoData? private var mapInfoNode: LocationInfoListItemNode? private var searchHeader: ComponentView? private var botPreviewLanguageTab: ComponentView? private var botPreviewFooter: ComponentView? private var barBackgroundLayer: SimpleLayer? private let itemGrid: SparseItemGrid private let itemGridBinding: SparseItemGridBindingImpl private let directMediaImageCache: DirectMediaImageCache private var items: SparseItemGrid.Items? private var pinnedIds: Set = Set() private var reorderedIds: [StoryId]? private var itemCount: Int? private var didUpdateItemsOnce: Bool = false private var selectionPanel: ComponentView? private var isDeceleratingAfterTracking = false private var _itemInteraction: VisualMediaItemInteraction? private var itemInteraction: VisualMediaItemInteraction { return self._itemInteraction! } public var selectedIds: Set { return self.itemInteraction.selectedIds ?? Set() } private let selectedIdsPromise = ValuePromise>(Set()) public var updatedSelectedIds: Signal, NoError> { return self.selectedIdsPromise.get() } private var isReordering: Bool = false public var selectedItems: [Int32: EngineStoryItem] { var result: [Int32: EngineStoryItem] = [:] for id in self.selectedIds { if let items = self.items { for item in items.items { if let item = item as? VisualMediaItem { if item.story.id == id { result[id] = item.story } } } } } return result } public var isEmpty: Bool { if let items = self.items, items.items.count != 0 { return false } else { return true } } public var isEmptyUpdated: (Bool) -> Void = { _ in } public private(set) var isSelectionModeActive: Bool private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)? private var listBottomInset: CGFloat? private let ready = Promise() private var didSetReady: Bool = false public var isReady: Signal { return self.ready.get() } private let statusPromise = Promise(nil) public var status: Signal { self.statusPromise.get() } public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? public var tabBarOffset: CGFloat { if case .botPreview = self.scope { return 0.0 } else { return self.itemGrid.coveringInsetOffset } } private var currentListState: StoryListContext.State? private var listDisposable: Disposable? private var hiddenMediaDisposable: Disposable? private let updateDisposable = MetaDisposable() private var currentBotPreviewLanguages: [StoryListContext.State.Language] = [] private var removedBotPreviewLanguages = Set() private var numberOfItemsToRequest: Int = 50 private var isRequestingView: Bool = false private var isFirstHistoryView: Bool = true private var decelerationAnimator: ConstantDisplayLinkAnimator? private var animationTimer: SwiftSignalKit.Timer? public private(set) var calendarSource: SparseMessageCalendar? private var listSource: StoryListContext private let maxBotPreviewCount: Int private let defaultListSource: StoryListContext private var cachedListSources: [String: StoryListContext] = [:] public var currentBotPreviewLanguage: (id: String, name: String)? { guard let listSource = self.listSource as? BotPreviewStoryListContext else { return nil } guard let id = listSource.language else { return nil } guard let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) else { return nil } return (language.id, language.name) } public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? public var emptyAction: (() -> Void)? public var additionalEmptyAction: (() -> Void)? public var ensureRectVisible: ((UIView, CGRect) -> Void)? private weak var currentGestureItem: SparseItemGridDisplayItem? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private weak var pendingOpenListContext: PeerStoryListContentContextImpl? private var preloadArchiveListContext: StoryListContext? private var emptyStateView: ComponentView? private weak var contextControllerToDismissOnSelection: ContextControllerProtocol? private weak var tempContextContentItemNode: TempExtractedItemNode? public var additionalNavigationHeight: CGFloat { if self.locationViewState.displayingMapModeOptions { return 38.0 } return 0.0 } public init(context: AccountContext, scope: Scope, captureProtected: Bool, isProfileEmbedded: Bool, canManageStories: Bool, navigationController: @escaping () -> NavigationController?, listContext: StoryListContext?) { self.context = context self.scope = scope self.navigationController = navigationController self.isProfileEmbedded = isProfileEmbedded self.canManageStories = canManageStories switch scope { case let .peer(_, _, isArchived): self.isSelectionModeActive = !isProfileEmbedded && isArchived default: self.isSelectionModeActive = false } self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.contextGestureContainerNode = ContextControllerSourceNode() self.itemGrid = SparseItemGrid(theme: self.presentationData.theme) self.directMediaImageCache = DirectMediaImageCache(account: context.account) self.itemGridBinding = SparseItemGridBindingImpl( context: context, directMediaImageCache: self.directMediaImageCache, captureProtected: captureProtected, displayPrivacy: isProfileEmbedded ) if let listContext { self.listSource = listContext } else { switch self.scope { case let .peer(id, _, isArchived): self.listSource = PeerStoryListContext(account: context.account, peerId: id, isArchived: isArchived) case let .search(peerId, query): self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(peerId, query)) case let .location(coordinates, venue): self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue))) case let .botPreview(id): self.listSource = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: id, language: nil, assumeEmpty: false) } } self.defaultListSource = self.listSource self.calendarSource = nil var maxBotPreviewCount = 10 if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double { maxBotPreviewCount = Int(value) } self.maxBotPreviewCount = maxBotPreviewCount super.init() if case .peer = self.scope { let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in guard let strongSelf = self else { return } if count < 1 { strongSelf.itemGrid.updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip(animation: "anim_infotip", text: strongSelf.itemGridBinding.chatPresentationData.strings.SharedMedia_FastScrollTooltip, completed: { guard let strongSelf = self else { return } let _ = ApplicationSpecificNotice.incrementSharedMediaScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager, count: 1).start() })) } }) } self.itemGridBinding.loadHoleImpl = { [weak self] hole, location in guard let strongSelf = self else { return .never() } return strongSelf.loadHole(anchor: hole, at: location) } self.itemGridBinding.onTapImpl = { [weak self] item, itemLayer, point in guard let self else { return } if self.isProfileEmbedded { if let selectedIds = self.itemInteraction.selectedIds { self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id)) return } } else { if let selectedIds = self.itemInteraction.selectedIds, let itemLayer = itemLayer as? ItemLayer, let selectionLayer = itemLayer.selectionLayer { if selectionLayer.checkLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) { self.itemInteraction.toggleSelection(item.story.id, !selectedIds.contains(item.story.id)) return } } } if self.pendingOpenListContext != nil { return } var splitIndexIntoDays = true switch self.scope { case .peer: break default: splitIndexIntoDays = false } let listContext = PeerStoryListContentContextImpl( context: self.context, listContext: self.listSource, initialId: item.storyId, splitIndexIntoDays: splitIndexIntoDays ) self.pendingOpenListContext = listContext self.itemGrid.isUserInteractionEnabled = false let _ = (listContext.state |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self, let navigationController = self.navigationController() else { return } guard let pendingOpenListContext = self.pendingOpenListContext, pendingOpenListContext === listContext else { return } self.pendingOpenListContext = nil self.itemGrid.isUserInteractionEnabled = true var transitionIn: StoryContainerScreen.TransitionIn? let story = item.story var foundItem: SparseItemGridDisplayItem? var foundItemLayer: SparseItemGridLayer? self.itemGrid.forEachVisibleItem { item in guard let itemLayer = item.layer as? ItemLayer else { return } foundItem = item if let listItem = itemLayer.item, listItem.story.id == story.id { foundItemLayer = itemLayer } } if let foundItemLayer { let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer) transitionIn = StoryContainerScreen.TransitionIn( sourceView: self.view, sourceRect: self.itemGrid.view.convert(itemRect, to: self.view), sourceCornerRadius: 0.0, sourceIsAvatar: false ) if let blurLayer = foundItem?.blurLayer { let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(layer: blurLayer, alpha: 0.0) } } let storyContainerScreen = StoryContainerScreen( context: self.context, content: listContext, transitionIn: transitionIn, transitionOut: { [weak self] _, itemId in guard let self else { return nil } var foundItem: SparseItemGridDisplayItem? var foundItemLayer: SparseItemGridLayer? self.itemGrid.forEachVisibleItem { item in guard let itemLayer = item.layer as? ItemLayer else { return } foundItem = item if let listItem = itemLayer.item, AnyHashable(listItem.story.id) == itemId { foundItemLayer = itemLayer } } if let foundItemLayer { if let blurLayer = foundItem?.blurLayer { let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(layer: blurLayer, alpha: 1.0) } let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer) return StoryContainerScreen.TransitionOut( destinationView: self.view, transitionView: StoryContainerScreen.TransitionView( makeView: { [weak foundItemLayer] in return ItemTransitionView(itemLayer: foundItemLayer) }, updateView: { view, state, transition in (view as? ItemTransitionView)?.update(state: state, transition: transition) }, insertCloneTransitionView: { [weak self] view in guard let self else { return } self.addToTransitionSurface(view: view) } ), destinationRect: self.itemGrid.view.convert(itemRect, to: self.view), destinationCornerRadius: 0.0, destinationIsAvatar: false, completed: {} ) } return nil } ) storyContainerScreen.performReorderAction = { [weak self] in guard let self else { return } self.beginReordering() } self.hiddenMediaDisposable?.dispose() self.hiddenMediaDisposable = (storyContainerScreen.focusedItem |> deliverOnMainQueue).start(next: { [weak self] itemId in guard let self else { return } if let itemId { let anyAmount = self.itemInteraction.hiddenStories.isEmpty self.itemInteraction.hiddenStories = Set([itemId]) if let items = self.items, let item = items.items.first(where: { $0.id == AnyHashable(itemId.id) }) { self.itemGrid.ensureItemVisible(index: item.index, anyAmount: anyAmount) if !anyAmount { var foundItemLayer: SparseItemGridLayer? self.itemGrid.forEachVisibleItem { item in guard let itemLayer = item.layer as? ItemLayer else { return } if let listItem = itemLayer.item, listItem.story.id == itemId.id { foundItemLayer = itemLayer } } if let foundItemLayer { self.ensureRectVisible?(self.view, self.itemGrid.frameForItem(layer: foundItemLayer)) } } } } else { self.itemInteraction.hiddenStories = Set() } self.updateHiddenItems() }) navigationController.pushViewController(storyContainerScreen) }) } self.itemGridBinding.onTagTapImpl = { [weak self] in guard let strongSelf = self else { return } strongSelf.openCurrentDate?() } self.itemGridBinding.reorderIfPossibleImpl = { [weak self] item, toIndex in guard let self else { return } self.reorderIfPossible(item: item, toIndex: toIndex) } self.itemGridBinding.didScrollImpl = { [weak self] in guard let strongSelf = self else { return } strongSelf.paneDidScroll?() strongSelf.cancelPreviewGestures() if strongSelf.locationViewState.displayingMapModeOptions { strongSelf.locationViewState.displayingMapModeOptions = false strongSelf.parentController?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } } self.itemGridBinding.coveringInsetOffsetUpdatedImpl = { [weak self] transition in guard let self else { return } self.tabBarOffsetUpdated?(transition) } self.itemGridBinding.scrollingOffsetUpdatedImpl = { [weak self] transition in guard let self else { return } self.gridScrollingOffsetUpdated(transition: transition) } var processedOnBeginFastScrolling = false self.itemGridBinding.onBeginFastScrollingImpl = { [weak self] in guard let strongSelf = self else { return } if processedOnBeginFastScrolling { return } processedOnBeginFastScrolling = true let _ = (ApplicationSpecificNotice.getSharedMediaFastScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { count in guard let strongSelf = self else { return } if count < 1 { let _ = ApplicationSpecificNotice.incrementSharedMediaFastScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager).start() var currentNode: ASDisplayNode = strongSelf var result: PeerInfoScreenNodeProtocol? while true { if let currentNode = currentNode as? PeerInfoScreenNodeProtocol { result = currentNode break } else if let supernode = currentNode.supernode { currentNode = supernode } else { break } } if let result = result { result.displaySharedMediaFastScrollingTooltip() } } }) } self.itemGridBinding.getShimmerColorsImpl = { [weak self] in guard let strongSelf = self, let presentationData = strongSelf.currentParams?.presentationData else { return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff) } if case .botPreview = scope { let backgroundColor = presentationData.theme.list.plainBackgroundColor let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) } else { let backgroundColor = presentationData.theme.list.mediaPlaceholderColor let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6) return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb) } } self.itemGridBinding.updateShimmerLayersImpl = { [weak self] layer in self?.itemGrid.updateShimmerLayers(item: layer) } self.itemGrid.cancelExternalContentGestures = { [weak self] in self?.contextGestureContainerNode.cancelGesture() } self.itemGrid.zoomLevelUpdated = { [weak self] zoomLevel in guard let strongSelf = self else { return } let _ = strongSelf //let _ = updateVisualMediaStoredState(engine: strongSelf.context.engine, peerId: strongSelf.peerId, messageTag: strongSelf.stateTag, state: VisualMediaStoredState(zoomLevel: Int32(zoomLevel.rawValue))).start() } self._itemInteraction = VisualMediaItemInteraction( openItem: { [weak self] _ in guard let self else { return } let _ = self }, openItemContextActions: { [weak self] item, sourceNode, sourceRect, gesture in guard let self else { return } let _ = self }, toggleSelection: { [weak self] id, value in guard let self, let itemInteraction = self._itemInteraction else { return } if let parentController = self.parentController as? PeerInfoScreen { parentController.toggleStorySelection(ids: [id], isSelected: value) } else { if var selectedIds = itemInteraction.selectedIds { if value { selectedIds.insert(id) } else { selectedIds.remove(id) } itemInteraction.selectedIds = selectedIds self.selectedIdsPromise.set(selectedIds) self.updateSelectedItems(animated: true) } } } ) if self.isSelectionModeActive { self._itemInteraction?.selectedIds = Set() } self.itemGridBinding.itemInteraction = self._itemInteraction self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded self.contextGestureContainerNode.addSubnode(self.itemGrid) self.addSubnode(self.contextGestureContainerNode) if case .location = scope { let mapNode = LocationMapHeaderNode( presentationData: self.presentationData, toggleMapModeSelection: { [weak self] in guard let self else { return } var state = self.locationViewState state.displayingMapModeOptions = !state.displayingMapModeOptions self.locationViewState = state }, goToUserLocation: { [weak self] in guard let self else { return } var state = self.locationViewState state.displayingMapModeOptions = false state.selectedLocation = .user switch state.trackingMode { case .none: state.trackingMode = .follow case .follow: state.trackingMode = .followWithHeading case .followWithHeading: state.trackingMode = .none } self.locationViewState = state } ) self.mapNode = mapNode self.addSubnode(mapNode) } self.contextGestureContainerNode.shouldBegin = { [weak self] point in guard let strongSelf = self else { return false } guard let item = strongSelf.itemGrid.item(at: point) else { return false } guard let layer = item.layer as? ItemLayer else { return false } guard let storyItem = layer.item else { return false } if let result = strongSelf.view.hitTest(point, with: nil) { if result.asyncdisplaykit_node is SparseItemGridScrollingArea { return false } } if !strongSelf.canManageStories { if !storyItem.story.isForwardingDisabled, case .everyone = storyItem.story.privacy?.base { } else { return false } } strongSelf.currentGestureItem = item return true } self.contextGestureContainerNode.customActivationProgress = { [weak self] progress, update in guard let strongSelf = self, let currentGestureItem = strongSelf.currentGestureItem else { return } guard let itemLayer = currentGestureItem.layer else { return } let targetContentRect = CGRect(origin: CGPoint(), size: itemLayer.bounds.size) let scaleSide = itemLayer.bounds.width let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide) let currentScale = 1.0 * (1.0 - progress) + minScale * progress let originalCenterOffsetX: CGFloat = itemLayer.bounds.width / 2.0 - targetContentRect.midX let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale let originalCenterOffsetY: CGFloat = itemLayer.bounds.height / 2.0 - targetContentRect.midY let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY switch update { case .update: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) itemLayer.transform = sublayerTransform case .begin: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) itemLayer.transform = sublayerTransform case .ended: let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0) let previousTransform = itemLayer.transform itemLayer.transform = sublayerTransform itemLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "transform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2) } } self.contextGestureContainerNode.activated = { [weak self] gesture, _ in guard let strongSelf = self, let currentGestureItem = strongSelf.currentGestureItem else { return } strongSelf.currentGestureItem = nil guard let itemLayer = currentGestureItem.layer as? ItemLayer else { return } guard let story = itemLayer.item?.story else { return } let rect = strongSelf.itemGrid.frameForItem(layer: itemLayer) strongSelf.openContextMenu(item: story, itemLayer: itemLayer, rect: rect, gesture: gesture) } let paneKey: PeerInfoPaneKey switch self.scope { case let .peer(_, _, isArchived): paneKey = isArchived ? .storyArchive : .stories case .botPreview: paneKey = .botPreview default: paneKey = .stories } self.statusPromise.set(.single(PeerInfoStatusData(text: "", isActivity: false, key: paneKey))) self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in guard let strongSelf = self else { return } strongSelf.itemGridBinding.updatePresentationData(presentationData: presentationData) strongSelf.itemGrid.updatePresentationData(theme: presentationData.theme) strongSelf.mapOptionsNode?.updatePresentationData(presentationData) }) self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false) if case let .peer(id, _, isArchived) = self.scope, id == context.account.peerId, !isArchived { self.preloadArchiveListContext = PeerStoryListContext(account: context.account, peerId: context.account.peerId, isArchived: true) } if case let .location(_, venue) = scope, let mapNode = self.mapNode { let locationCoordinate = CLLocationCoordinate2D(latitude: venue.latitude, longitude: venue.longitude) var initialMapState = LocationViewState() initialMapState.selectedLocation = .coordinate(locationCoordinate, true) self.locationViewStatePromise.set(.single(initialMapState)) let userLocation: Signal = .single(nil) |> then( throttledUserLocation(mapNode.mapNode.userLocation) ) var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating)) var address: Signal = .single(nil) let locale = localeWithStrings(self.presentationData.strings) eta = .single((.calculating, .calculating, .calculating)) |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .automobile), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .transit), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .walking)) |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in if case .calculating = drivingTime { return .complete() } if case .calculating = transitTime { return .complete() } if case .calculating = walkingTime { return .complete() } return .single((drivingTime, transitTime, walkingTime)) }) /*if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { address = .single(venueAddress) } else*/ do { address = .single(nil) |> then( reverseGeocodeLocation(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude, locale: locale) |> map { placemark -> String? in return placemark?.compactDisplayAddress ?? "" } ) } let previousState = Atomic(value: nil) let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) self.mapDisposable = (combineLatest( context.sharedContext.presentationData, self.locationViewStatePromise.get(), mapNode.mapNode.userLocation, userLocation, address, eta ) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, distance, address, eta in guard let self, let mapNode = self.mapNode else { return } let previousState = previousState.swap(state) var annotations: [LocationPinAnnotation] = [] let subjectLocation = CLLocation(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude) let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } let locationMap = TelegramMediaMap(latitude: locationCoordinate.latitude, longitude: locationCoordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, address: venue.address, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let mapInfoData = MapInfoData( location: locationMap, address: address, distance: distance, drivingTime: eta.0, transitTime: eta.1, walkingTime: eta.2, hasEta: false ) annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, location: locationMap, queryId: nil, resultId: nil, forcedSelection: true)) mapNode.updateState( mapMode: state.mapMode, trackingMode: state.trackingMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: nil, animated: false ) mapNode.mapNode.trackingMode = state.trackingMode let previousAnnotations = previousAnnotations.swap(annotations) if annotations != previousAnnotations { mapNode.mapNode.annotations = annotations } switch state.selectedLocation { case .initial: if previousState?.selectedLocation != .initial { mapNode.mapNode.setMapCenter(coordinate: locationCoordinate, span: LocationMapNode.viewMapSpan, animated: previousState != nil) } case let .coordinate(coordinate, defaultSpan): if let previousState = previousState, case let .coordinate(previousCoordinate, _) = previousState.selectedLocation, locationCoordinatesAreEqual(previousCoordinate, coordinate) { } else { mapNode.mapNode.setMapCenter( coordinate: coordinate, span: defaultSpan ? LocationMapNode.defaultMapSpan : LocationMapNode.viewMapSpan, animated: true ) } case .user: if previousState?.selectedLocation != .user, let userLocation = userLocation { mapNode.mapNode.setMapCenter( coordinate: userLocation.coordinate, isUserLocation: true, animated: true ) } case .custom: break } if let previousState, previousState.displayingMapModeOptions != state.displayingMapModeOptions { self.parentController?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } else { if self.mapInfoData != mapInfoData { self.mapInfoData = mapInfoData self.update(transition: .immediate) } } }) } } deinit { self.listDisposable?.dispose() self.hiddenMediaDisposable?.dispose() self.animationTimer?.invalidate() self.presentationDataDisposable?.dispose() self.updateDisposable.dispose() self.mapDisposable?.dispose() } public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { let listSource = self.listSource return Signal { subscriber in listSource.loadMore(completion: { Queue.mainQueue().async { subscriber.putCompletion() } }) return EmptyDisposable } |> runOn(.mainQueue()) } private func openContextMenu(item: EngineStoryItem, itemLayer: ItemLayer, rect: CGRect, gesture: ContextGesture?) { guard let parentController = self.parentController else { return } let canManage = self.canManageStories var items: [ContextMenuItem] = [] if canManage, case let .peer(peerId, _, isArchived) = self.scope { items.append(.action(ContextMenuActionItem(text: !isArchived ? self.presentationData.strings.StoryList_ItemAction_Archive : self.presentationData.strings.StoryList_ItemAction_Unarchive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Unarchive"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in guard let self else { f(.default) return } if isArchived { f(.default) } else { f(.dismissWithoutContent) } let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: [item.id: item], isPinned: isArchived ? true : false).startStandalone() self.parentController?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: isArchived ? self.presentationData.strings.StoryList_ToastUnarchived_Text(1) : self.presentationData.strings.StoryList_ToastArchived_Text(1), cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) if !isArchived { let isPinned = self.pinnedIds.contains(item.id) items.append(.action(ContextMenuActionItem(text: isPinned ? self.presentationData.strings.StoryList_ItemAction_Unpin : self.presentationData.strings.StoryList_ItemAction_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak itemLayer] _, f in itemLayer?.isHidden = false guard let self else { f(.default) return } if !isPinned && self.pinnedIds.count >= 3 { f(.default) let presentationData = self.presentationData self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.StoryList_ToastPinLimit_Text(Int32(3)), timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) return } f(.dismissWithoutContent) var updatedPinnedIds = self.pinnedIds if isPinned { updatedPinnedIds.remove(item.id) } else { updatedPinnedIds.insert(item.id) } let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: peerId, ids: Array(updatedPinnedIds)).startStandalone() let presentationData = self.presentationData let toastTitle: String? let toastText: String if isPinned { toastTitle = nil toastText = presentationData.strings.StoryList_ToastUnpinned_Text(1) } else { toastTitle = presentationData.strings.StoryList_ToastPinned_Title(1) toastText = presentationData.strings.StoryList_ToastPinned_Text(1) } self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: isPinned ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) }))) if isPinned && self.canReorder() { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self else { return } self.beginReordering() }) }))) } } items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Edit, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self else { return } let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let self, let peer else { return } var foundItemLayer: SparseItemGridLayer? var sourceImage: UIImage? self.itemGrid.forEachVisibleItem { gridItem in guard let itemLayer = gridItem.layer as? ItemLayer else { return } if let listItem = itemLayer.item, listItem.story.id == item.id { foundItemLayer = itemLayer if let contents = itemLayer.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID { sourceImage = UIImage(cgImage: contents as! CGImage) } } } guard let controller = MediaEditorScreen.makeEditStoryController( context: self.context, peer: peer, storyItem: item, videoPlaybackPosition: nil, cover: false, repost: false, transitionIn: .gallery(MediaEditorScreen.TransitionIn.GalleryTransitionIn(sourceView: self.itemGrid.view, sourceRect: foundItemLayer?.frame ?? .zero, sourceImage: sourceImage)), transitionOut: MediaEditorScreen.TransitionOut(destinationView: self.itemGrid.view, destinationRect: foundItemLayer?.frame ?? .zero, destinationCornerRadius: 0.0), update: { [weak self] disposable in guard let self else { return } self.updateDisposable.set(disposable) } ) else { return } self.parentController?.push(controller) }) }) }))) } if canManage, case .botPreview = self.scope, self.canReorder() { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.BotPreviews_MenuReorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self else { return } self.beginReordering() }) }))) } if !item.isForwardingDisabled, case .everyone = item.privacy?.base { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Forward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self, case let .peer(peerId, _, _) = self.scope else { return } let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let self else { return } guard let peer, let peerReference = PeerReference(peer._asPeer()) else { return } let shareController = ShareController( context: self.context, subject: .media(.story(peer: peerReference, id: item.id, media: TelegramMediaStory(storyId: StoryId(peerId: peer.id, id: item.id), isMention: false))), presetText: nil, preferredAction: .default, showInChat: nil, fromForeignApp: false, segmentedValues: nil, externalShare: false, immediateExternalShare: false, switchableAccounts: [], immediatePeerId: nil, updatedPresentationData: nil, forceTheme: nil, forcedActionTitle: nil, shareAsLink: false, collectibleItemInfo: nil ) self.parentController?.present(shareController, in: .window(.root)) }) }) }))) } if canManage { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in c?.dismiss(completion: { guard let self else { return } self.presentDeleteConfirmation(ids: Set([item.id])) }) }))) } if self.canManageStories { if !items.isEmpty { items.append(.separator) } items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, f in guard let self, let parentController = self.parentController as? PeerInfoScreen else { f(.default) return } self.contextControllerToDismissOnSelection = c parentController.toggleStorySelection(ids: [item.id], isSelected: true) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { [weak self] in guard let self, let contextControllerToDismissOnSelection = self.contextControllerToDismissOnSelection else { return } if let contextControllerToDismissOnSelection = contextControllerToDismissOnSelection as? ContextController { contextControllerToDismissOnSelection.dismissWithCustomTransition(transition: .animated(duration: 0.4, curve: .spring), completion: nil) } }) }))) } if items.isEmpty { return } let tempSourceNode = TempExtractedItemNode( item: item, itemLayer: itemLayer ) tempSourceNode.frame = rect tempSourceNode.update(size: rect.size) let scaleSide = itemLayer.bounds.width let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide) let currentScale = minScale ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: tempSourceNode.contextSourceNode.contentNode, scale: currentScale) ContainedViewLayoutTransition.immediate.updateTransformScale(layer: itemLayer, scale: 1.0) self.tempContextContentItemNode = tempSourceNode self.addSubnode(tempSourceNode) let contextController = ContextController(presentationData: self.presentationData, source: .extracted(ExtractedContentSourceImpl(controller: parentController, sourceNode: tempSourceNode.contextSourceNode, keepInPlace: false, blurBackground: true)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) parentController.presentInGlobalOverlay(contextController) } public func updateZoomLevel(level: ZoomLevel) { self.itemGrid.setZoomLevel(level: level.value) //let _ = updateVisualMediaStoredState(engine: self.context.engine, peerId: self.peerId, messageTag: self.stateTag, state: VisualMediaStoredState(zoomLevel: level.rawValue)).start() } public func setIsSelectionModeActive(_ value: Bool) { if self.isSelectionModeActive != value { self.isSelectionModeActive = value if value { if self._itemInteraction?.selectedIds == nil { self._itemInteraction?.selectedIds = Set() } } else { self._itemInteraction?.selectedIds = nil } self.selectedIdsPromise.set(self._itemInteraction?.selectedIds ?? Set()) self.updateSelectedItems(animated: true) } } public func ensureMessageIsVisible(id: MessageId) { } private func requestHistoryAroundVisiblePosition(synchronous: Bool, reloadAtTop: Bool) { if self.isRequestingView { return } self.isRequestingView = true var firstTime = true let queue = Queue() let state = self.listSource.state self.listDisposable?.dispose() self.listDisposable = nil if reloadAtTop { self.didUpdateItemsOnce = false } self.listDisposable = (state |> deliverOn(queue)).startStrict(next: { [weak self] state in guard let self else { return } let title: String if state.totalCount == 0 { if case .botPreview = self.scope { if state.isLoading { title = self.presentationData.strings.BotPreviews_SubtitleLoading } else { title = self.presentationData.strings.BotPreviews_SubtitleEmpty } } else { title = "" } } else if case let .peer(_, isSaved, isArchived) = self.scope { if isSaved { title = self.presentationData.strings.StoryList_SubtitleSaved(Int32(state.totalCount)) } else if isArchived { title = self.presentationData.strings.StoryList_SubtitleArchived(Int32(state.totalCount)) } else { title = self.presentationData.strings.StoryList_SubtitleCount(Int32(state.totalCount)) } } else if case .botPreview = self.scope { title = self.presentationData.strings.BotPreviews_SubtitleCount(Int32(state.totalCount)) } else if case .search = self.scope { title = self.presentationData.strings.StoryList_SubtitleCount(Int32(state.totalCount)) } else { title = "" } let paneKey: PeerInfoPaneKey switch self.scope { case let .peer(_, _, isArchived): paneKey = isArchived ? .storyArchive : .stories case .botPreview: paneKey = .botPreview default: paneKey = .stories } self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: paneKey))) Queue.mainQueue().async { [weak self] in guard let self else { return } var botPreviewLanguages = self.currentBotPreviewLanguages for language in state.availableLanguages { if !botPreviewLanguages.contains(where: { $0.id == language.id }) && !self.removedBotPreviewLanguages.contains(language.id) { botPreviewLanguages.append(language) } } botPreviewLanguages.sort(by: { $0.name < $1.name }) var hadLocalItems = false if let currentListState = self.currentListState { for item in currentListState.items { if item.storyItem.isPending { hadLocalItems = true } } } self.currentListState = state var hasLocalItems = false if let currentListState = self.currentListState { for item in currentListState.items { if item.storyItem.isPending { hasLocalItems = true } } } var synchronous = synchronous if hasLocalItems != hadLocalItems { synchronous = true } self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false) if self.currentBotPreviewLanguages != botPreviewLanguages || reloadAtTop { self.currentBotPreviewLanguages = botPreviewLanguages if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: .immediate) } } firstTime = false self.isRequestingView = false } }) } private func updateItemsFromState(state: StoryListContext.State, firstTime: Bool, reloadAtTop: Bool, synchronous: Bool, animated: Bool) { let timezoneOffset = Int32(TimeZone.current.secondsFromGMT()) var mappedItems: [SparseItemGrid.Item] = [] var mappedHoles: [SparseItemGrid.HoleAnchor] = [] var totalCount: Int = 0 var stateItems = state.items if let reorderedIds = self.reorderedIds { var fixedStateItems: [StoryListContext.State.Item] = [] var seenIds = Set() for id in reorderedIds { if let index = stateItems.firstIndex(where: { $0.id == id }) { seenIds.insert(id) fixedStateItems.append(stateItems[index]) } } for item in stateItems { if !seenIds.contains(item.id) { fixedStateItems.append(item) } } stateItems = fixedStateItems self.reorderedIds = fixedStateItems.map(\.id) } for item in stateItems { var peerReference: PeerReference? if let value = state.peerReference { peerReference = value } else if let peer = item.peer { peerReference = PeerReference(peer._asPeer()) } guard let peerReference else { continue } var authorPeer = item.peer var isReorderable = false switch self.scope { case .botPreview: isReorderable = !item.storyItem.isPending case let .peer(id, _, _): if id == self.context.account.peerId { isReorderable = state.pinnedIds.contains(item.storyItem.id) } case let .search(peerId, _): if peerId != nil { authorPeer = nil } default: break } mappedItems.append(VisualMediaItem( index: mappedItems.count, peer: peerReference, storyId: item.id, story: item.storyItem, authorPeer: authorPeer, isPinned: state.pinnedIds.contains(item.storyItem.id), localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue, isReorderable: isReorderable )) } if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken { mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: StoryId(peerId: context.account.peerId, id: Int32.max), localMonthTimestamp: Month(localTimestamp: lastItem.storyItem.timestamp + timezoneOffset).packedValue)) } totalCount = state.totalCount totalCount = max(mappedItems.count, totalCount) if totalCount == 0 && state.loadMoreToken != nil && !state.isCached { totalCount = 100 } var headerText: String? if case let .peer(peerId, _, isArchived) = self.scope { if isArchived && !mappedItems.isEmpty && peerId == self.context.account.peerId { headerText = self.presentationData.strings.StoryList_ArchiveDescription } } let items = SparseItemGrid.Items( items: mappedItems, holeAnchors: mappedHoles, count: totalCount, itemBinding: self.itemGridBinding, headerText: headerText, snapTopInset: false ) self.itemCount = state.totalCount let currentSynchronous = synchronous && firstTime let currentReloadAtTop = reloadAtTop && firstTime self.updateHistory(items: items, pinnedIds: Set(state.pinnedIds), synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop, animated: animated) } private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set, synchronous: Bool, reloadAtTop: Bool, animated: Bool) { var transition: ContainedViewLayoutTransition = .immediate if case .location = self.scope, let previousItems = self.items, previousItems.items.count == 0, previousItems.count != 0, items.items.count == 0, items.count == 0 { transition = .animated(duration: 0.3, curve: .spring) } self.items = items self.pinnedIds = pinnedIds if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { var gridSnapshot: UIView? if case .botPreview = scope { } else if reloadAtTop { gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition, animateGridItems: animated) self.updateSelectedItems(animated: false) if let gridSnapshot = gridSnapshot { self.view.addSubview(gridSnapshot) gridSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak gridSnapshot] _ in gridSnapshot?.removeFromSuperview() }) } } self.isEmptyUpdated(self.isEmpty) if !self.didSetReady { self.didSetReady = true self.ready.set(.single(true)) } } private func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) { if let items = self.items, let item = item as? VisualMediaItem { var toIndex = toIndex if case .botPreview = self.scope { } else if case let .peer(id, _, _) = self.scope { if id == self.context.account.peerId { let maxPinnedIndex = items.items.lastIndex(where: { ($0 as? VisualMediaItem)?.isPinned == true }) if let maxPinnedIndex { toIndex = min(toIndex, maxPinnedIndex) } else { return } } } else { return } guard let toItem = items.items.first(where: { $0.index == toIndex }) as? VisualMediaItem else { return } if item.story.isPending || toItem.story.isPending { return } if !item.isReorderable { return } var ids = items.items.compactMap { item -> StoryId? in return (item as? VisualMediaItem)?.storyId } if let fromIndex = ids.firstIndex(of: item.storyId) { if fromIndex < toIndex { ids.insert(item.storyId, at: toIndex + 1) ids.remove(at: fromIndex) } else if fromIndex > toIndex { ids.remove(at: fromIndex) ids.insert(item.storyId, at: toIndex) } } if self.reorderedIds != ids { self.reorderedIds = ids HapticFeedback().tap() if let currentListState = self.currentListState { self.updateItemsFromState(state: currentListState, firstTime: false, reloadAtTop: false, synchronous: false, animated: true) } } } } public func scrollToTop() -> Bool { return self.itemGrid.scrollToTop() } public func hitTestResultForScrolling() -> UIView? { return self.itemGrid.hitTestResultForScrolling() } public func brieflyDisableTouchActions() { self.itemGrid.brieflyDisableTouchActions() } public func findLoadedMessage(id: MessageId) -> Message? { return nil } public func updateHiddenMedia() { } public func transferVelocity(_ velocity: CGFloat) { self.itemGrid.transferVelocity(velocity) } public func cancelPreviewGestures() { } public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } public func extractPendingStoryTransitionView() -> UIView? { guard let items = self.items else { return nil } guard let visualItem = items.items.last(where: { item in guard let item = item as? VisualMediaItem else { return false } if item.story.isPending { return true } return false }) else { return nil } guard let item = self.itemGrid.item(at: visualItem.index) else { return nil } guard let itemLayer = item.layer as? ItemLayer else { return nil } guard let story = itemLayer.item?.story else { return nil } let rect = self.itemGrid.frameForItem(layer: itemLayer) let tempSourceNode = TempExtractedItemNode( item: story, itemLayer: itemLayer ) tempSourceNode.frame = rect tempSourceNode.update(size: rect.size) self.tempContextContentItemNode = tempSourceNode self.view.addSubview(tempSourceNode.view) return tempSourceNode.view } public func addToTransitionSurface(view: UIView) { self.itemGrid.addToTransitionSurface(view: view) } private var gridSelectionGesture: MediaPickerGridSelectionGesture? override public func didLoad() { super.didLoad() /*let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) selectionRecognizer.shouldBegin = { return true } self.view.addGestureRecognizer(selectionRecognizer)*/ } private var selectionPanState: (selecting: Bool, initialMessageId: EngineMessage.Id, toggledMessageIds: [[EngineMessage.Id]])? private var selectionScrollActivationTimer: SwiftSignalKit.Timer? private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator? private var selectionScrollDelta: CGFloat? private var selectionLastLocation: CGPoint? private func storyAtPoint(_ location: CGPoint) -> StoryViewList.Item? { return nil } @objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void { //TODO:selection /*let location = recognizer.location(in: self.view) switch recognizer.state { case .began: if let message = self.messageAtPoint(location) { let selecting = !(self.chatControllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false) self.selectionPanState = (selecting, message.id, []) self.chatControllerInteraction.toggleMessagesSelection([message.id], selecting) } case .changed: self.handlePanSelection(location: location) self.selectionLastLocation = location case .ended, .failed, .cancelled: self.selectionPanState = nil self.selectionScrollDisplayLink = nil self.selectionScrollActivationTimer?.invalidate() self.selectionScrollActivationTimer = nil self.selectionScrollDelta = nil self.selectionLastLocation = nil self.selectionScrollSkipUpdate = false case .possible: break @unknown default: fatalError() }*/ } private func handlePanSelection(location: CGPoint) { } private var selectionScrollSkipUpdate = false private func setupSelectionScrolling() { self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in self?.selectionScrollActivationTimer = nil if let strongSelf = self, let delta = strongSelf.selectionScrollDelta { let distance: CGFloat = 15.0 * min(1.0, 0.15 + abs(delta * delta)) let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down let _ = strongSelf.itemGrid.scrollWithDelta(direction == .up ? -distance : distance) if let location = strongSelf.selectionLastLocation { if !strongSelf.selectionScrollSkipUpdate { strongSelf.handlePanSelection(location: location) } strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate } } }) self.selectionScrollDisplayLink?.isPaused = false } override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { /*let location = gestureRecognizer.location(in: gestureRecognizer.view) if location.x < 44.0 { return false }*/ return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { let _ = otherGestureRecognizer //otherGestureRecognizer.isEnabled = false //otherGestureRecognizer.isEnabled = true return true } else { return false } } public func clearSelection() { self.itemInteraction.selectedIds = Set() self.selectedIdsPromise.set(Set()) self.updateSelectedItems(animated: true) } public func updateSelectedMessages(animated: Bool) { } public func updateSelectedStories(selectedStoryIds: Set?, animated: Bool) { self.itemInteraction.selectedIds = selectedStoryIds self.selectedIdsPromise.set(selectedStoryIds ?? Set()) self.updateSelectedItems(animated: animated) if let tempContextContentItemNode = self.tempContextContentItemNode, let itemLayer = tempContextContentItemNode.itemLayer { let rect = self.itemGrid.frameForItem(layer: itemLayer) tempContextContentItemNode.frame = rect var isSelected: Bool? if let selectedIds = self.itemInteraction.selectedIds { isSelected = selectedIds.contains(tempContextContentItemNode.item.id) } tempContextContentItemNode.itemView.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: isSelected, animated: true) } if let contextControllerToDismissOnSelection = self.contextControllerToDismissOnSelection as? ContextController { self.contextControllerToDismissOnSelection = nil contextControllerToDismissOnSelection.dismissWithCustomTransition(transition: .animated(duration: 0.4, curve: .spring), completion: nil) } if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .animated(duration: 0.4, curve: .spring)) } } private func updateSelectedItems(animated: Bool) { self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil && !self.isReordering self.itemGrid.forEachVisibleItem { item in guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else { return } itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.itemInteraction.selectedIds?.contains(item.story.id), animated: animated) } var isSelecting = false if let selectedIds = self._itemInteraction?.selectedIds, !selectedIds.isEmpty { isSelecting = true } self.itemGrid.pinchEnabled = !isSelecting var enableDismissGesture = true if self.isProfileEmbedded { enableDismissGesture = true } else if let items = self.items, items.items.isEmpty { } if isSelecting { enableDismissGesture = false } self.view.disablesInteractiveTransitionGestureRecognizer = !enableDismissGesture if isSelecting { if self.gridSelectionGesture == nil { let selectionGesture = MediaPickerGridSelectionGesture() selectionGesture.delegate = self.wrappedGestureRecognizerDelegate selectionGesture.sideInset = 44.0 selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in self?.itemGrid.isScrollEnabled = isEnabled } selectionGesture.itemAt = { [weak self] point in if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let storyId = itemLayer.item?.story.id { return (storyId, strongSelf._itemInteraction?.selectedIds?.contains(storyId) ?? false) } else { return nil } } selectionGesture.updateSelection = { [weak self] storyId, selected in if let strongSelf = self { strongSelf._itemInteraction?.toggleSelection(storyId, selected) } } self.itemGrid.view.addGestureRecognizer(selectionGesture) self.gridSelectionGesture = selectionGesture } } else if let gridSelectionGesture = self.gridSelectionGesture { self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture) self.gridSelectionGesture = nil } } private func updateHiddenItems() { self.itemGrid.forEachVisibleItem { itemValue in guard let itemLayer = itemValue.layer as? ItemLayer, let item = itemLayer.item else { return } let itemHidden = self.itemInteraction.hiddenStories.contains(item.storyId) itemLayer.isHidden = itemHidden if let blurLayer = itemValue.blurLayer { let transition = ComponentTransition.immediate if itemHidden { transition.setAlpha(layer: blurLayer, alpha: 0.0) } else { transition.setAlpha(layer: blurLayer, alpha: 1.0) } } } } private func presentDeleteConfirmation(ids: Set) { if case let .peer(peerId, _, _) = self.scope { let presentationData = self.presentationData let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } let title: String = presentationData.strings.StoryList_DeleteConfirmation_Title(Int32(ids.count)) controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: title), ActionSheetButtonItem(title: presentationData.strings.StoryList_DeleteConfirmation_Action, color: .destructive, action: { [weak self] in dismissAction() guard let self else { return } if let parentController = self.parentController as? PeerInfoScreen { parentController.cancelItemSelection() } let _ = self.context.engine.messages.deleteStories(peerId: peerId, ids: Array(ids)).startStandalone() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self.parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } else if case let .botPreview(peerId) = self.scope { let presentationData = self.presentationData let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } var mappedMedia: [Media] = [] if let items = self.items { mappedMedia = items.items.compactMap { item -> Media? in guard let item = item as? VisualMediaItem else { return nil } if ids.contains(item.story.id) { return item.story.media._asMedia() } else { return nil } } } if mappedMedia.isEmpty { return } let title: String = presentationData.strings.BotPreviews_SheetDeleteTitle(Int32(mappedMedia.count)) controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: title), ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self] in dismissAction() guard let self else { return } guard let listSource = self.listSource as? BotPreviewStoryListContext else { return } if let parentController = self.parentController as? PeerInfoScreen { parentController.cancelItemSelection() } let _ = self.context.engine.messages.deleteBotPreviews(peerId: peerId, language: listSource.language, media: mappedMedia).startStandalone() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self.parentController?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } private func update(transition: ContainedViewLayoutTransition) { if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition) } } private func gridScrollingOffsetUpdated(transition: ContainedViewLayoutTransition) { if let currentParams = self.currentParams { if let _ = self.mapNode { self.updateMapLayout(size: currentParams.size, topInset: currentParams.topInset, bottomInset: currentParams.bottomInset, deviceMetrics: currentParams.deviceMetrics, transition: transition) } if case .botPreview = self.scope, self.canManageStories { self.updateBotPreviewLanguageTab(size: currentParams.size, topInset: currentParams.topInset, transition: transition) self.updateBotPreviewFooter(size: currentParams.size, bottomInset: 0.0, transition: transition) } } } private var effectiveMapHeight: CGFloat = 0.0 private func updateMapLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { guard let mapNode = self.mapNode else { return } var mapHeight = min(size.width, size.height) mapHeight = min(mapHeight, floor(size.height * 0.389)) let mapOverscrollInset: CGFloat = size.height - mapHeight self.effectiveMapHeight = mapHeight - self.additionalNavigationHeight let mapSize = CGSize(width: size.width, height: mapHeight + mapOverscrollInset) var controlsTopPadding = mapOverscrollInset + self.additionalNavigationHeight let effectiveScrollingOffset: CGFloat if let items = self.items, items.items.isEmpty, items.count == 0 { effectiveScrollingOffset = -size.height * 0.5 + 60.0 + bottomInset } else { effectiveScrollingOffset = self.itemGrid.scrollingOffset } controlsTopPadding += min(0.0, effectiveScrollingOffset) let mapFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - mapOverscrollInset - effectiveScrollingOffset - self.additionalNavigationHeight), size: mapSize) transition.updateFrame(node: mapNode, frame: mapFrame) let mapOffset = min(floorToScreenPixels(effectiveScrollingOffset * 0.5), mapSize.height) mapNode.updateLayout( layout: ContainerViewLayout( size: mapSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: deviceMetrics, intrinsicInsets: UIEdgeInsets(top: mapOverscrollInset, left: 0.0, bottom: 0.0, right: 0.0), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false ), navigationBarHeight: 0.0, topPadding: mapOverscrollInset + self.additionalNavigationHeight, controlsTopPadding: controlsTopPadding, offset: mapOffset, size: mapSize, transition: transition ) if let mapInfoData = self.mapInfoData { let mapInfoNode: LocationInfoListItemNode if let current = self.mapInfoNode { mapInfoNode = current } else { mapInfoNode = LocationInfoListItemNode() mapInfoNode.isUserInteractionEnabled = false self.mapInfoNode = mapInfoNode mapNode.supernode?.insertSubnode(mapInfoNode, aboveSubnode: mapNode) mapInfoNode.clipsToBounds = true mapInfoNode.cornerRadius = 10.0 mapInfoNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } mapInfoNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor let addressString: String? if let address = mapInfoData.address { addressString = address } else { addressString = self.presentationData.strings.Map_Locating } let distanceString: String? if let distance = mapInfoData.distance { distanceString = distance < 10 ? self.presentationData.strings.Map_YouAreHere : self.presentationData.strings.Map_DistanceAway(stringForDistance(strings: self.presentationData.strings, distance: distance)).string } else { distanceString = nil } let item = LocationInfoListItem( presentationData: ItemListPresentationData(self.presentationData), engine: self.context.engine, location: mapInfoData.location, address: addressString, distance: distanceString, drivingTime: mapInfoData.drivingTime, transitTime: mapInfoData.transitTime, walkingTime: mapInfoData.walkingTime, hasEta: mapInfoData.hasEta, action: {}, drivingAction: {}, transitAction: {}, walkingAction: {} ) let (mapInfoLayout, mapInfoReadyAndApply) = mapInfoNode.asyncLayout()( item, ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 1000.0, isStandalone: true) ) let mapInfoTopInset: CGFloat = -6.0 let mapInfoFrame = CGRect(origin: CGPoint(x: 0.0, y: mapFrame.maxY + mapInfoTopInset), size: mapInfoLayout.contentSize) transition.updateFrame(node: mapInfoNode, frame: mapInfoFrame) mapInfoReadyAndApply().1(ListViewItemApply(isOnScreen: true)) self.effectiveMapHeight += mapInfoLayout.contentSize.height + mapInfoTopInset if let itemCount = self.itemCount, itemCount != 0 { let searchHeader: ComponentView if let current = self.searchHeader { searchHeader = current } else { searchHeader = ComponentView() self.searchHeader = searchHeader } let searchHeaderSize = searchHeader.update( transition: ComponentTransition(transition), component: AnyComponent(StorySearchHeaderComponent( theme: self.presentationData.theme, strings: self.presentationData.strings, count: itemCount )), environment: {}, containerSize: CGSize(width: size.width, height: 1000.0) ) let searchHeaderFrame = CGRect(origin: CGPoint(x: 0.0, y: max(topInset, mapInfoFrame.maxY)), size: searchHeaderSize) if let searchHeaderView = searchHeader.view { if searchHeaderView.superview == nil { self.view.addSubview(searchHeaderView) } transition.updateFrame(view: searchHeaderView, frame: searchHeaderFrame) } self.effectiveMapHeight += searchHeaderSize.height } else { if let searchHeader = self.searchHeader { self.searchHeader = nil searchHeader.view?.removeFromSuperview() } } } else { if let mapInfoNode = self.mapInfoNode { self.mapInfoNode = nil mapInfoNode.removeFromSupernode() } if let searchHeader = self.searchHeader { self.searchHeader = nil searchHeader.view?.removeFromSuperview() } } } private func updateBotPreviewLanguageTab(size: CGSize, topInset: CGFloat, transition: ContainedViewLayoutTransition) { guard case .botPreview = self.scope, self.canManageStories else { return } let botPreviewLanguageTab: ComponentView if let current = self.botPreviewLanguageTab { botPreviewLanguageTab = current } else { botPreviewLanguageTab = ComponentView() self.botPreviewLanguageTab = botPreviewLanguageTab } var languageItems: [TabSelectorComponent.Item] = [] languageItems.append(TabSelectorComponent.Item( id: AnyHashable("_main"), title: self.presentationData.strings.BotPreviews_LanguageTab_Main )) for language in self.currentBotPreviewLanguages { languageItems.append(TabSelectorComponent.Item( id: AnyHashable(language.id), title: language.name )) } languageItems.append(TabSelectorComponent.Item( id: AnyHashable("_add"), title: self.presentationData.strings.BotPreviews_LanguageTab_Add )) var selectedLanguageId = "_main" if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { selectedLanguageId = language } let botPreviewLanguageTabSize = botPreviewLanguageTab.update( transition: ComponentTransition(transition), component: AnyComponent(TabSelectorComponent( colors: TabSelectorComponent.Colors( foreground: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.8), selection: self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05) ), customLayout: TabSelectorComponent.CustomLayout( font: Font.medium(14.0), spacing: 9.0, verticalInset: 11.0 ), items: languageItems, selectedId: AnyHashable(selectedLanguageId), setSelectedId: { [weak self] id in guard let self, let id = id.base as? String else { return } if id == "_add" { self.presentAddBotPreviewLanguage() } else if id == "_main" { self.setBotPreviewLanguage(id: nil, assumeEmpty: false) } else if let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { self.setBotPreviewLanguage(id: language.id, assumeEmpty: false) } } )), environment: {}, containerSize: CGSize(width: size.width, height: 44.0) ) var botPreviewLanguageTabFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewLanguageTabSize.width) * 0.5), y: topInset - 11.0), size: botPreviewLanguageTabSize) let effectiveScrollingOffset: CGFloat effectiveScrollingOffset = self.itemGrid.scrollingOffset botPreviewLanguageTabFrame.origin.y -= effectiveScrollingOffset let isSelectingOrReordering = self.isReordering || self.itemInteraction.selectedIds != nil if let botPreviewLanguageTabView = botPreviewLanguageTab.view { if botPreviewLanguageTabView.superview == nil { self.view.addSubview(botPreviewLanguageTabView) } transition.updateFrame(view: botPreviewLanguageTabView, frame: botPreviewLanguageTabFrame) transition.updateAlpha(layer: botPreviewLanguageTabView.layer, alpha: isSelectingOrReordering ? 0.5 : 1.0) botPreviewLanguageTabView.isUserInteractionEnabled = !isSelectingOrReordering } } private func updateBotPreviewFooter(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { if let items = self.items, !items.items.isEmpty { var botPreviewFooterTransition = ComponentTransition(transition) let botPreviewFooter: ComponentView if let current = self.botPreviewFooter { botPreviewFooter = current } else { botPreviewFooterTransition = .immediate botPreviewFooter = ComponentView() self.botPreviewFooter = botPreviewFooter } var isMainLanguage = true let text: String if let listSource = self.listSource as? BotPreviewStoryListContext, let id = listSource.language, let language = self.currentBotPreviewLanguages.first(where: { $0.id == id }) { isMainLanguage = false text = self.presentationData.strings.BotPreviews_TranslationFooter_Text(language.name).string } else { text = self.presentationData.strings.BotPreviews_DefaultFooter_Text } let botPreviewFooterSize = botPreviewFooter.update( transition: botPreviewFooterTransition, component: AnyComponent(EmptyStateIndicatorComponent( context: self.context, theme: self.presentationData.theme, fitToHeight: true, animationName: nil, title: nil, text: text, actionTitle: self.presentationData.strings.BotPreviews_Empty_Add, action: { [weak self] in guard let self else { return } if self.canAddMoreBotPreviews() { self.emptyAction?() } else { self.presentUnableToAddMorePreviewsAlert() } }, additionalActionTitle: isMainLanguage ? self.presentationData.strings.BotPreviews_Empty_AddTranslation : nil, additionalAction: { [weak self] in guard let self else { return } if isMainLanguage { self.presentAddBotPreviewLanguage() } }, additionalActionSeparator: isMainLanguage ? self.presentationData.strings.BotPreviews_Empty_Separator : nil )), environment: {}, containerSize: CGSize(width: size.width, height: 1000.0) ) let botPreviewFooterFrame = CGRect(origin: CGPoint(x: floor((size.width - botPreviewFooterSize.width) * 0.5), y: self.itemGrid.contentBottomOffset + 16.0), size: botPreviewFooterSize) if let botPreviewFooterView = botPreviewFooter.view { if botPreviewFooterView.superview == nil { self.view.addSubview(botPreviewFooterView) } botPreviewFooterTransition.setFrame(view: botPreviewFooterView, frame: botPreviewFooterFrame) } } else { if let botPreviewFooter = self.botPreviewFooter { self.botPreviewFooter = nil botPreviewFooter.view?.removeFromSuperview() } } } public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition, animateGridItems: false) } private func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition, animateGridItems: Bool) { self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) var gridTopInset = topInset if self.mapNode != nil { self.updateMapLayout(size: size, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, transition: transition) gridTopInset += self.effectiveMapHeight let mapOptionsNode: LocationOptionsNode if let current = self.mapOptionsNode { mapOptionsNode = current } else { mapOptionsNode = LocationOptionsNode(presentationData: self.presentationData, hasBackground: false, updateMapMode: { [weak self] mode in guard let self else { return } var state = self.locationViewState state.mapMode = mode state.displayingMapModeOptions = false self.locationViewState = state }) mapOptionsNode.clipsToBounds = true self.mapOptionsNode = mapOptionsNode self.parentController?.navigationBar?.additionalContentNode.addSubnode(mapOptionsNode) } let mapOptionsFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - self.additionalNavigationHeight), size: CGSize(width: size.width, height: self.additionalNavigationHeight)) transition.updatePosition(node: mapOptionsNode, position: mapOptionsFrame.center) transition.updateBounds(node: mapOptionsNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: 38.0 - self.additionalNavigationHeight), size: mapOptionsFrame.size)) mapOptionsNode.updateLayout(size: mapOptionsFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition) } if self.isProfileEmbedded, case .botPreview = self.scope { let barBackgroundLayer: SimpleLayer if let current = self.barBackgroundLayer { barBackgroundLayer = current } else { barBackgroundLayer = SimpleLayer() self.barBackgroundLayer = barBackgroundLayer self.layer.insertSublayer(barBackgroundLayer, at: 0) } barBackgroundLayer.backgroundColor = presentationData.theme.list.plainBackgroundColor.cgColor transition.updateFrame(layer: barBackgroundLayer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: gridTopInset))) } var listBottomInset = bottomInset var bottomInset = bottomInset if case .botPreview = self.scope, self.canManageStories { updateBotPreviewLanguageTab(size: size, topInset: topInset, transition: transition) gridTopInset += 50.0 updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) if let botPreviewFooterView = self.botPreviewFooter?.view { listBottomInset += 18.0 + botPreviewFooterView.bounds.height } } if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case let .peer(peerId, _, isArchived) = self.scope { let selectionPanel: ComponentView var selectionPanelTransition = ComponentTransition(transition) if let current = self.selectionPanel { selectionPanel = current } else { selectionPanelTransition = selectionPanelTransition.withAnimation(.none) selectionPanel = ComponentView() self.selectionPanel = selectionPanel } var selectionItems: [BottomActionsPanelComponent.Item] = [] let actionIsPin = !selectedIds.contains(where: { self.pinnedIds.contains($0) }) selectionItems.append(BottomActionsPanelComponent.Item( id: "pin-unpin", color: .accent, title: actionIsPin ? presentationData.strings.StoryList_ActionPanel_Pin : presentationData.strings.StoryList_ActionPanel_Unpin, isEnabled: !selectedIds.isEmpty, action: { [weak self] in guard let self, let selectedIds = self.itemInteraction.selectedIds else { return } if !selectedIds.contains(where: { self.pinnedIds.contains($0) }) { var updatedPinnedIds = self.pinnedIds for id in selectedIds { updatedPinnedIds.insert(id) } if updatedPinnedIds.count > 3 { let presentationData = self.presentationData let animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor let toastText = presentationData.strings.StoryList_ToastPinLimit_Text(3) self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_infotip", scale: 1.0, colors: ["info1.info1.stroke": animationBackgroundColor, "info2.info2.Fill": animationBackgroundColor], title: nil, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) } else { let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: peerId, ids: Array(updatedPinnedIds)).startStandalone() let presentationData = self.presentationData let toastTitle = presentationData.strings.StoryList_ToastPinned_Title(Int32(selectedIds.count)) let toastText = presentationData.strings.StoryList_ToastPinned_Text(Int32(selectedIds.count)) self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) if let parentController = self.parentController as? PeerInfoScreen { parentController.cancelItemSelection() } } } else { var updatedPinnedIds = self.pinnedIds for id in selectedIds { updatedPinnedIds.remove(id) } let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: peerId, ids: Array(updatedPinnedIds)).startStandalone() let presentationData = self.presentationData let toastTitle: String? = nil let toastText: String = presentationData.strings.StoryList_ToastUnpinned_Text(Int32(selectedIds.count)) self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_toastunpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) if let parentController = self.parentController as? PeerInfoScreen { parentController.cancelItemSelection() } } } )) selectionItems.append(BottomActionsPanelComponent.Item( id: "archive", color: .accent, title: isArchived ? presentationData.strings.StoryList_ActionPanel_Unarchive : presentationData.strings.StoryList_ActionPanel_Archive, isEnabled: !selectedIds.isEmpty, action: { [weak self] in guard let self, let _ = self.itemInteraction.selectedIds else { return } let items: [Int32: EngineStoryItem] = self.selectedItems if let parentController = self.parentController as? PeerInfoScreen { parentController.cancelItemSelection() } let _ = self.context.engine.messages.updateStoriesArePinned(peerId: peerId, ids: items, isPinned: isArchived ? true : false).startStandalone() let text: String if isArchived { text = presentationData.strings.StoryList_ToastUnarchived_Text(Int32(items.count)) } else { text = presentationData.strings.StoryList_ToastArchived_Text(Int32(items.count)) } self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: text, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) } )) selectionItems.append(BottomActionsPanelComponent.Item( id: "delete", color: .destructive, title: presentationData.strings.StoryList_ActionPanel_Delete, isEnabled: !selectedIds.isEmpty, action: { [weak self] in guard let self, let selectedIds = self.itemInteraction.selectedIds else { return } self.presentDeleteConfirmation(ids: selectedIds) } )) let selectionPanelSize = selectionPanel.update( transition: selectionPanelTransition, component: AnyComponent(BottomActionsPanelComponent( theme: presentationData.theme, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), items: selectionItems )), environment: {}, containerSize: size ) let selectionPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - selectionPanelSize.height), size: selectionPanelSize) if let selectionPanelView = selectionPanel.view { if selectionPanelView.superview == nil { self.view.addSubview(selectionPanelView) transition.animatePositionAdditive(layer: selectionPanelView.layer, offset: CGPoint(x: 0.0, y: selectionPanelFrame.height)) } selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) } bottomInset = selectionPanelSize.height listBottomInset += selectionPanelSize.height } else if self.isProfileEmbedded, let selectedIds = self.itemInteraction.selectedIds, self.canManageStories, case .botPreview = self.scope { let selectionPanel: ComponentView var selectionPanelTransition = ComponentTransition(transition) if let current = self.selectionPanel { selectionPanel = current } else { selectionPanelTransition = selectionPanelTransition.withAnimation(.none) selectionPanel = ComponentView() self.selectionPanel = selectionPanel } var selectionItems: [BottomActionsPanelComponent.Item] = [] selectionItems.append(BottomActionsPanelComponent.Item( id: "delete", color: .destructive, title: presentationData.strings.StoryList_ActionPanel_Delete, isEnabled: !selectedIds.isEmpty, action: { [weak self] in guard let self, let selectedIds = self.itemInteraction.selectedIds else { return } self.presentDeleteConfirmation(ids: selectedIds) } )) let selectionPanelSize = selectionPanel.update( transition: selectionPanelTransition, component: AnyComponent(BottomActionsPanelComponent( theme: presentationData.theme, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), items: selectionItems )), environment: {}, containerSize: size ) let selectionPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - selectionPanelSize.height), size: selectionPanelSize) if let selectionPanelView = selectionPanel.view { if selectionPanelView.superview == nil { self.view.addSubview(selectionPanelView) transition.animatePositionAdditive(layer: selectionPanelView.layer, offset: CGPoint(x: 0.0, y: selectionPanelFrame.height)) } selectionPanelTransition.setFrame(view: selectionPanelView, frame: selectionPanelFrame) } bottomInset = selectionPanelSize.height listBottomInset += selectionPanelSize.height } else if let selectionPanel = self.selectionPanel { self.selectionPanel = nil if let selectionPanelView = selectionPanel.view { transition.updateFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: selectionPanelView.bounds.size)) } } transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) if case let .peer(_, _, isArchived) = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { let emptyStateView: ComponentView var emptyStateTransition = ComponentTransition(transition) if let current = self.emptyStateView { emptyStateView = current } else { emptyStateTransition = .immediate emptyStateView = ComponentView() self.emptyStateView = emptyStateView } let emptyStateSize = emptyStateView.update( transition: emptyStateTransition, component: AnyComponent(EmptyStateIndicatorComponent( context: self.context, theme: presentationData.theme, fitToHeight: self.isProfileEmbedded, animationName: "StoryListEmpty", title: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, text: isArchived ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, actionTitle: isArchived ? nil : presentationData.strings.StoryList_SavedAddAction, action: { [weak self] in guard let self else { return } self.emptyAction?() }, additionalActionTitle: (isArchived || self.isProfileEmbedded) ? nil : presentationData.strings.StoryList_SavedEmptyAction, additionalAction: { [weak self] in guard let self else { return } self.additionalEmptyAction?() } )), environment: {}, containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) ) let emptyStateFrame: CGRect if self.isProfileEmbedded { emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) } else { emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) } if let emptyStateComponentView = emptyStateView.view { if emptyStateComponentView.superview == nil { self.view.addSubview(emptyStateComponentView) if self.didUpdateItemsOnce { emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } emptyStateTransition.setFrame(view: emptyStateComponentView, frame: emptyStateFrame) } let backgroundColor: UIColor if self.isProfileEmbedded { backgroundColor = presentationData.theme.list.plainBackgroundColor } else { backgroundColor = presentationData.theme.list.blocksBackgroundColor } if self.didUpdateItemsOnce { ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor) } else { self.view.backgroundColor = backgroundColor } } else if case .botPreview = self.scope, let items = self.items, items.items.isEmpty, items.count == 0 { let emptyStateView: ComponentView var emptyStateTransition = ComponentTransition(transition) if let current = self.emptyStateView { emptyStateView = current } else { emptyStateTransition = .immediate emptyStateView = ComponentView() self.emptyStateView = emptyStateView } var isMainLanguage = true if let listSource = self.listSource as? BotPreviewStoryListContext, let _ = listSource.language { isMainLanguage = false } let emptyStateSize = emptyStateView.update( transition: emptyStateTransition, component: AnyComponent(EmptyStateIndicatorComponent( context: self.context, theme: presentationData.theme, fitToHeight: self.isProfileEmbedded, animationName: nil, title: presentationData.strings.BotPreviews_Empty_Title, text: presentationData.strings.BotPreviews_Empty_Text(Int32(self.maxBotPreviewCount)), actionTitle: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Add : nil, action: { [weak self] in guard let self else { return } if self.canAddMoreBotPreviews() { self.emptyAction?() } else { self.presentUnableToAddMorePreviewsAlert() } }, additionalActionTitle: self.canManageStories ? (isMainLanguage ? presentationData.strings.BotPreviews_Empty_AddTranslation : presentationData.strings.BotPreviews_Empty_DeleteTranslation) : nil, additionalAction: { if isMainLanguage { self.presentAddBotPreviewLanguage() } else { self.presentDeleteBotPreviewLanguage() } }, additionalActionSeparator: self.canManageStories ? presentationData.strings.BotPreviews_Empty_Separator : nil )), environment: {}, containerSize: CGSize(width: size.width, height: size.height - gridTopInset - bottomInset) ) let emptyStateFrame: CGRect if self.isProfileEmbedded { emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: max(gridTopInset + 22.0, floor((visibleHeight - gridTopInset - bottomInset - emptyStateSize.height) * 0.5))), size: emptyStateSize) } else { emptyStateFrame = CGRect(origin: CGPoint(x: floor((size.width - emptyStateSize.width) * 0.5), y: gridTopInset), size: emptyStateSize) } if let emptyStateComponentView = emptyStateView.view { if emptyStateComponentView.superview == nil { self.view.addSubview(emptyStateComponentView) if self.didUpdateItemsOnce { emptyStateComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } emptyStateTransition.setFrame(view: emptyStateComponentView, frame: emptyStateFrame) } let backgroundColor: UIColor if self.isProfileEmbedded, case .botPreview = self.scope { backgroundColor = presentationData.theme.list.blocksBackgroundColor } else if self.isProfileEmbedded { backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { backgroundColor = presentationData.theme.list.blocksBackgroundColor } if self.didUpdateItemsOnce { ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)).setBackgroundColor(view: self.view, color: backgroundColor) } else { self.view.backgroundColor = backgroundColor } } else { if let emptyStateView = self.emptyStateView { let subTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) self.emptyStateView = nil if let emptyStateComponentView = emptyStateView.view { if self.didUpdateItemsOnce { subTransition.setAlpha(view: emptyStateComponentView, alpha: 0.0, completion: { [weak emptyStateComponentView] _ in emptyStateComponentView?.removeFromSuperview() }) } else { emptyStateComponentView.removeFromSuperview() } } if self.isProfileEmbedded, case .botPreview = self.scope { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) } else if self.isProfileEmbedded { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.plainBackgroundColor) } else { subTransition.setBackgroundColor(view: self.view, color: presentationData.theme.list.blocksBackgroundColor) } } else { if self.isProfileEmbedded, case .botPreview = self.scope { self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { if case let .search(peerId, _) = self.scope, peerId != nil { } else { self.view.backgroundColor = .clear } } } } transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) if let items = self.items { let wasFirstTime = !self.didUpdateItemsOnce self.didUpdateItemsOnce = true let fixedItemHeight: CGFloat? let isList = false fixedItemHeight = nil let fixedItemAspect: CGFloat? = 0.81 var adjustForSmallCount = true if case .botPreview = self.scope { adjustForSmallCount = false } self.itemGrid.pinchEnabled = items.count > 2 && !self.isReordering self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: listBottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, adjustForSmallCount: adjustForSmallCount, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate) } self.listBottomInset = listBottomInset if case .botPreview = self.scope, self.canManageStories { updateBotPreviewFooter(size: size, bottomInset: 0.0, transition: transition) } } public func currentTopTimestamp() -> Int32? { var timestamp: Int32? self.itemGrid.forEachVisibleItem { item in guard let itemLayer = item.layer as? ItemLayer else { return } if let item = itemLayer.item { if let timestampValue = timestamp { timestamp = max(timestampValue, item.story.timestamp) } else { timestamp = item.story.timestamp } } } return timestamp } public func scrollToTimestamp(timestamp: Int32) { if let items = self.items, !items.items.isEmpty { var previousIndex: Int? for item in items.items { guard let item = item as? VisualMediaItem else { continue } if item.story.timestamp <= timestamp { break } previousIndex = item.index } if previousIndex == nil { previousIndex = (items.items[0] as? VisualMediaItem)?.index } if let index = previousIndex { self.itemGrid.scrollToItem(at: index) if let item = self.itemGrid.item(at: index) { if let layer = item.layer as? ItemLayer { Queue.mainQueue().after(0.1, { [weak layer] in guard let layer = layer else { return } let overlayLayer = SimpleLayer() overlayLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.6).cgColor overlayLayer.frame = layer.bounds layer.addSublayer(overlayLayer) overlayLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.8, delay: 0.3, removeOnCompletion: false, completion: { [weak overlayLayer] _ in overlayLayer?.removeFromSuperlayer() }) }) } } } } } public func scrollToItem(index: Int) { guard let _ = self.items else { return } self.itemGrid.scrollToItem(at: index) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let result = super.hitTest(point, with: event) else { return nil } /*if self.decelerationAnimator != nil { self.decelerationAnimator?.isPaused = true self.decelerationAnimator = nil return self.scrollNode.view }*/ return result } public func availableZoomLevels() -> (decrement: ZoomLevel?, increment: ZoomLevel?) { let levels = self.itemGrid.availableZoomLevels() return (levels.decrement.flatMap(ZoomLevel.init), levels.increment.flatMap(ZoomLevel.init)) } public func canAddMoreBotPreviews() -> Bool { guard let items = self.items else { return false } return items.count < self.maxBotPreviewCount } public func canReorder() -> Bool { guard let items = self.items else { return false } return items.count > 1 } private func presentAddBotPreviewLanguage() { let excludeIds: [String] = self.currentBotPreviewLanguages.map(\.id) self.parentController?.push(LanguageSelectionScreen(context: self.context, excludeIds: excludeIds, selectLocalization: { [weak self] info in guard let self else { return } self.addBotPreviewLanguage(language: StoryListContext.State.Language(id: info.languageCode, name: info.title)) })) } public func presentUnableToAddMorePreviewsAlert() { self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.BotPreviews_AlertTooManyPreviews(Int32(self.maxBotPreviewCount)), actions: [ TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { }) ], parseMarkdown: true), in: .window(.root)) } public func presentDeleteBotPreviewLanguage() { self.parentController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [ TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in guard let self else { return } if let listSource = self.listSource as? BotPreviewStoryListContext, let language = listSource.language { self.deleteBotPreviewLanguage(id: language) } }) ], parseMarkdown: true), in: .window(.root)) } private func addBotPreviewLanguage(language: StoryListContext.State.Language) { var botPreviewLanguages = self.currentBotPreviewLanguages var assumeEmpty = false if !botPreviewLanguages.contains(where: { $0.id == language.id}) { botPreviewLanguages.append(language) assumeEmpty = true } botPreviewLanguages.sort(by: { $0.name < $1.name }) self.removedBotPreviewLanguages.remove(language.id) if self.currentBotPreviewLanguages != botPreviewLanguages { self.currentBotPreviewLanguages = botPreviewLanguages if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate) } } self.setBotPreviewLanguage(id: language.id, assumeEmpty: assumeEmpty) } private func deleteBotPreviewLanguage(id: String) { var botPreviewLanguages = self.currentBotPreviewLanguages if let index = botPreviewLanguages.firstIndex(where: { $0.id == id}) { botPreviewLanguages.remove(at: index) } self.removedBotPreviewLanguages.insert(id) guard case let .botPreview(peerId) = self.scope else { return } var mappedMedia: [Media] = [] if let items = self.items { mappedMedia = items.items.compactMap { item -> Media? in guard let item = item as? VisualMediaItem else { return nil } return item.story.media._asMedia() } } let _ = self.context.engine.messages.deleteBotPreviewsLanguage(peerId: peerId, language: id, media: mappedMedia).startStandalone() if self.currentBotPreviewLanguages != botPreviewLanguages { self.currentBotPreviewLanguages = botPreviewLanguages if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: .immediate) } } self.setBotPreviewLanguage(id: nil, assumeEmpty: false) } private func setBotPreviewLanguage(id: String?, assumeEmpty: Bool) { guard case let .botPreview(peerId) = self.scope else { return } if let listSource = self.listSource as? BotPreviewStoryListContext, listSource.language == id { return } if let id { if let cachedListSource = self.cachedListSources[id] { self.listSource = cachedListSource } else { let listSource = BotPreviewStoryListContext(account: self.context.account, engine: self.context.engine, peerId: peerId, language: id, assumeEmpty: assumeEmpty) self.listSource = listSource self.cachedListSources[id] = listSource } } else { self.listSource = self.defaultListSource } self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: true) } public func beginReordering() { if let parentController = self.parentController as? PeerInfoScreen { parentController.togglePaneIsReordering(isReordering: true) } else { self.updateIsReordering(isReordering: true, animated: true) } } public func endReordering() { if let parentController = self.parentController as? PeerInfoScreen { parentController.togglePaneIsReordering(isReordering: false) } else { self.updateIsReordering(isReordering: false, animated: true) } } public func updateIsReordering(isReordering: Bool, animated: Bool) { if self.isReordering != isReordering { self.isReordering = isReordering self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil && !self.isReordering self.itemGrid.setReordering(isReordering: isReordering) if !isReordering, let reorderedIds = self.reorderedIds { self.reorderedIds = nil if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext { if let items = self.items { var reorderedMedia: [Media] = [] for id in reorderedIds { if let item = items.items.first(where: { ($0 as? VisualMediaItem)?.storyId == id }) as? VisualMediaItem { reorderedMedia.append(item.story.media._asMedia()) } } listSource.reorderItems(media: reorderedMedia) } } else if case let .peer(id, _, _) = self.scope, id == self.context.account.peerId, let items = self.items { var updatedPinnedIds: [Int32] = [] for id in reorderedIds { inner: for item in items.items { if let item = item as? VisualMediaItem { if item.storyId == id { if item.isPinned { updatedPinnedIds.append(id.id) break inner } } } } } let _ = self.context.engine.messages.updatePinnedToTopStories(peerId: id, ids: updatedPinnedIds).startStandalone() } } self.update(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) } } } private class MediaListSelectionRecognizer: UIPanGestureRecognizer { private let selectionGestureActivationThreshold: CGFloat = 5.0 var recognized: Bool? = nil var initialLocation: CGPoint = CGPoint() public var shouldBegin: (() -> Bool)? public override init(target: Any?, action: Selector?) { super.init(target: target, action: action) self.minimumNumberOfTouches = 2 self.maximumNumberOfTouches = 2 } public override func reset() { super.reset() self.recognized = nil } public override func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) if let shouldBegin = self.shouldBegin, !shouldBegin() { self.state = .failed } else { let touch = touches.first! self.initialLocation = touch.location(in: self.view) } } public override func touchesMoved(_ touches: Set, with event: UIEvent) { let location = touches.first!.location(in: self.view) let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y) let touchesArray = Array(touches) if self.recognized == nil, touchesArray.count == 2 { if let firstTouch = touchesArray.first, let secondTouch = touchesArray.last { let firstLocation = firstTouch.location(in: self.view) let secondLocation = secondTouch.location(in: self.view) func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat { let dx = v1.x - v2.x let dy = v1.y - v2.y return sqrt(dx * dx + dy * dy) } if distance(firstLocation, secondLocation) > 200.0 { self.state = .failed } } if self.state != .failed && (abs(translation.y) >= selectionGestureActivationThreshold) { self.recognized = true } } if let recognized = self.recognized, recognized { super.touchesMoved(touches, with: event) } } } private final class TempExtractedItemNode: ASDisplayNode { let contextSourceNode: ContextExtractedContentContainingNode let item: EngineStoryItem weak var itemLayer: ItemLayer? let itemView: ItemTransitionView init(item: EngineStoryItem, itemLayer: ItemLayer) { self.item = item self.contextSourceNode = ContextExtractedContentContainingNode() self.itemView = ItemTransitionView(itemLayer: itemLayer) self.itemLayer = itemLayer super.init() self.addSubnode(self.contextSourceNode) self.contextSourceNode.contentNode.view.addSubview(self.itemView) self.itemView.clipsToBounds = true self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in guard let self else { return } transition.updateCornerRadius(layer: self.itemView.layer, cornerRadius: isExtracted ? 10.0 : 0.0) if isExtracted { transition.updateSublayerTransformScale(node: self.contextSourceNode.contentNode, scale: 1.0) } } self.contextSourceNode.isExtractedToContextPreviewUpdated = { [weak self] isExtracted in guard let self else { return } self.itemLayer?.isHidden = isExtracted if !isExtracted { self.removeFromSupernode() } } } func update(size: CGSize) { self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size) self.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) self.contextSourceNode.contentRect = CGRect(origin: CGPoint(x: 2.0, y: 0.0), size: CGSize(width: size.width - 4.0, height: size.height)) self.itemView.frame = CGRect(origin: CGPoint(), size: size) self.itemView.update(state: StoryContainerScreen.TransitionState(sourceSize: size, destinationSize: size, progress: 0.0), transition: .immediate) } } private final class ExtractedContentSourceImpl: ContextExtractedContentSource { var keepInPlace: Bool let ignoreContentTouches: Bool = true let blurBackground: Bool let adjustContentForSideInset: Bool = true private let controller: ViewController private let sourceNode: ContextExtractedContentContainingNode var actionsHorizontalAlignment: ContextActionsHorizontalAlignment { return .center } init(controller: ViewController, sourceNode: ContextExtractedContentContainingNode, keepInPlace: Bool, blurBackground: Bool) { self.controller = controller self.sourceNode = sourceNode self.keepInPlace = keepInPlace self.blurBackground = blurBackground } func takeView() -> ContextControllerTakeViewInfo? { return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) } func putBack() -> ContextControllerPutBackViewInfo? { return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) } } private final class BottomActionsPanelComponent: Component { public final class Item: Equatable { public enum Color { case accent case destructive } public var id: AnyHashable public var color: Color public var title: String public var isEnabled: Bool public var action: () -> Void public init(id: AnyHashable, color: Color, title: String, isEnabled: Bool, action: @escaping () -> Void) { self.id = id self.color = color self.title = title self.isEnabled = isEnabled self.action = action } public static func ==(lhs: Item, rhs: Item) -> Bool { if lhs === rhs { return true } if lhs.id != rhs.id { return false } if lhs.color != rhs.color { return false } if lhs.title != rhs.title { return false } if lhs.isEnabled != rhs.isEnabled { return false } return true } } public let theme: PresentationTheme public let insets: UIEdgeInsets public let items: [Item] public init( theme: PresentationTheme, insets: UIEdgeInsets, items: [Item] ) { self.theme = theme self.insets = insets self.items = items } public static func ==(lhs: BottomActionsPanelComponent, rhs: BottomActionsPanelComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.insets != rhs.insets { return false } if lhs.items != rhs.items { return false } return true } public final class View: UIView { private let backgroundView: BlurredBackgroundView private let separatorLayer: SimpleLayer private var itemViews: [AnyHashable: ComponentView] = [:] private var component: BottomActionsPanelComponent? public override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.backgroundView.isUserInteractionEnabled = false self.separatorLayer = SimpleLayer() super.init(frame: frame) self.addSubview(self.backgroundView) self.layer.addSublayer(self.separatorLayer) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: BottomActionsPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component if themeUpdated { self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor } let itemHeight: CGFloat = 54.0 let size = CGSize(width: availableSize.width, height: itemHeight + component.insets.bottom) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) self.backgroundView.update(size: size, transition: transition.containedViewLayoutTransition) transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) let sideInset = component.insets.left + 12.0 var validIds: [AnyHashable] = [] var itemsAndSizes: [(CGSize, ComponentView)] = [] for item in component.items { validIds.append(item.id) let itemColor: UIColor if item.isEnabled { switch item.color { case .accent: itemColor = component.theme.list.itemAccentColor case .destructive: itemColor = component.theme.list.itemDestructiveColor } } else { itemColor = component.theme.list.itemDisabledTextColor } let itemView: ComponentView if let current = self.itemViews[item.id] { itemView = current } else { itemView = ComponentView() self.itemViews[item.id] = itemView } let itemSize = itemView.update( transition: .immediate, component: AnyComponent(PlainButtonComponent( content: AnyComponent(Text(text: item.title, font: Font.regular(17.0), color: itemColor)), effectAlignment: .center, minSize: CGSize(width: 16.0, height: itemHeight), contentInsets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0), action: { item.action() }, isEnabled: item.isEnabled, animateAlpha: true, animateScale: false, animateContents: false )), environment: {}, containerSize: CGSize(width: availableSize.width, height: itemHeight) ) itemsAndSizes.append((itemSize, itemView)) } var removedIds: [AnyHashable] = [] for (id, itemView) in self.itemViews { if !validIds.contains(id) { removedIds.append(id) itemView.view?.removeFromSuperview() } } for id in removedIds { self.itemViews.removeValue(forKey: id) } for i in 0 ..< itemsAndSizes.count { let (itemSize, itemView) = itemsAndSizes[i] let itemCenterX: CGFloat = CGFloat(i) * (floor((availableSize.width - sideInset * 2.0) / CGFloat(itemsAndSizes.count - 1))) let itemX: CGFloat if itemsAndSizes.count == 1 { itemX = floor((availableSize.width - itemSize.width) * 0.5) } else if i == 0 { itemX = sideInset } else if i == itemsAndSizes.count - 1 { itemX = availableSize.width - sideInset - itemSize.width } else { itemX = sideInset + floor(itemCenterX - itemSize.width * 0.5) } let itemFrame = CGRect(origin: CGPoint(x: itemX, y: 0.0), size: itemSize) if let itemComponenView = itemView.view { if itemComponenView.superview == nil { self.addSubview(itemComponenView) } itemComponenView.frame = itemFrame } } return size } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }