diff --git a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift index 6f4e122c3d..a7b16cf5c4 100644 --- a/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift +++ b/submodules/ChatListUI/Sources/ChatListContainerItemNode.swift @@ -55,7 +55,7 @@ final class ChatListContainerItemNode: ASDisplayNode { private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)? private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)? - init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool) { + init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool, isMainTab: Bool?) { self.context = context self.controller = controller self.location = location @@ -68,7 +68,7 @@ final class ChatListContainerItemNode: ASDisplayNode { self.openArchiveSettings = openArchiveSettings self.isInlineMode = isInlineMode - self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady) + self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady, isMainTab: isMainTab) if let controller, case .chatList(groupId: .root) = controller.location { self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 7bad9c9780..967149393a 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -440,7 +440,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self?.secondaryEmptyAction() }, openArchiveSettings: { [weak self] in self?.openArchiveSettings() - }, autoSetReady: true) + }, autoSetReady: true, isMainTab: nil) self.itemNodes[.all] = itemNode self.addSubnode(itemNode) @@ -784,7 +784,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self?.secondaryEmptyAction() }, openArchiveSettings: { [weak self] in self?.openArchiveSettings() - }, autoSetReady: !animated) + }, autoSetReady: !animated, isMainTab: index == 0) let disposable = MetaDisposable() self.pendingItemNode = (id, itemNode, disposable) @@ -930,7 +930,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self?.secondaryEmptyAction() }, openArchiveSettings: { [weak self] in self?.openArchiveSettings() - }, autoSetReady: false) + }, autoSetReady: false, isMainTab: i == 0) itemNode.listNode.tempTopInset = self.tempTopInset self.itemNodes[id] = itemNode } @@ -969,6 +969,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } } + itemNode.listNode.isMainTab.set(self.availableFilters.firstIndex(where: { $0.id == id }) == 0 ? true : false) itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: itemInlineNavigationTransitionFraction, storiesInset: storiesInset, transition: nodeTransition) if let scrollingOffset = self.scrollingOffset { itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: nodeTransition) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 7f15d49781..e8c7b61eb7 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2358,6 +2358,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { break } } + storyStatsIds.removeAll(where: { $0 == context.account.peerId }) return context.engine.data.subscribe( EngineDataMap( @@ -2551,6 +2552,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> map { stats -> ([RecentlySearchedPeer], [EnginePeer.Id: PeerStoryStats]) in var mappedStats: [EnginePeer.Id: PeerStoryStats] = [:] for (id, value) in stats { + if id == context.account.peerId { + continue + } if let value { mappedStats[id] = value } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index fa42d71ae5..72a595d0ba 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -718,7 +718,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL } case let .buttonChoice(isPositive): switch notice { - case let .reviewLogin(newSessionReview): + case let .reviewLogin(newSessionReview, _): nodeInteraction?.performActiveSessionAction(newSessionReview, isPositive) default: break @@ -1038,7 +1038,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL } case let .buttonChoice(isPositive): switch notice { - case let .reviewLogin(newSessionReview): + case let .reviewLogin(newSessionReview, _): nodeInteraction?.performActiveSessionAction(newSessionReview, isPositive) default: break @@ -1250,6 +1250,7 @@ public final class ChatListNode: ListView { private let chatFolderUpdates = Promise() private var pollFilterUpdatesDisposable: Disposable? private var chatFilterUpdatesDisposable: Disposable? + private var updateIsMainTabDisposable: Disposable? public var scrollHeightTopInset: CGFloat { didSet { @@ -1261,7 +1262,10 @@ public final class ChatListNode: ListView { private let autoSetReady: Bool - public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool) { + public let isMainTab = ValuePromise(false, ignoreRepeated: true) + private let suggestedChatListNotice = Promise(nil) + + public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool, isMainTab: Bool?) { self.context = context self.location = location self.chatListFilter = chatListFilter @@ -1272,7 +1276,9 @@ public final class ChatListNode: ListView { self.animationRenderer = animationRenderer self.autoSetReady = autoSetReady - let isMainTab = chatListFilter == nil && location == .chatList(groupId: .root) + if let isMainTab { + self.isMainTab.set(isMainTab) + } var isSelecting = false if case .peers(_, true, _, _, _, _) = mode { @@ -1749,76 +1755,84 @@ public final class ChatListNode: ListView { displayArchiveIntro = .single(false) } - let suggestedChatListNotice: Signal - if case .chatList(groupId: .root) = location, chatListFilter == nil { + self.updateIsMainTabDisposable = (self.isMainTab.get() + |> deliverOnMainQueue).start(next: { [weak self] isMainTab in + guard let self else { + return + } + + guard case .chatList(groupId: .root) = location, isMainTab else { + self.suggestedChatListNotice.set(.single(nil)) + return + } + let _ = context.engine.privacy.cleanupSessionReviews().start() - suggestedChatListNotice = .single(nil) - |> then ( - combineLatest( - getServerProvidedSuggestions(account: context.account), - context.engine.auth.twoStepVerificationConfiguration(), - newSessionReviews(postbox: context.account.postbox) - ) - |> mapToSignal { suggestions, configuration, newSessionReviews -> Signal in - if let newSessionReview = newSessionReviews.first { - return .single(.reviewLogin(newSessionReview: newSessionReview)) + let twoStepData: Signal = .single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map(Optional.init)) + + let suggestedChatListNoticeSignal: Signal = combineLatest( + getServerProvidedSuggestions(account: context.account), + twoStepData, + newSessionReviews(postbox: context.account.postbox) + ) + |> mapToSignal { suggestions, configuration, newSessionReviews -> Signal in + if let newSessionReview = newSessionReviews.first { + return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count)) + } + if suggestions.contains(.setupPassword), let configuration { + var notSet = false + switch configuration { + case let .notSet(pendingEmail): + if pendingEmail == nil { + notSet = true + } + case .set: + break } - if suggestions.contains(.setupPassword) { - var notSet = false - switch configuration { - case let .notSet(pendingEmail): - if pendingEmail == nil { - notSet = true - } - case .set: - break - } - if notSet { - return .single(.setupPassword) - } - } - if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager { - return inAppPurchaseManager.availableProducts - |> map { products -> ChatListNotice? in - if products.count > 1 { - let shortestOptionPrice: (Int64, NSDecimalNumber) - if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) { - shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) - } else { - shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) - } - for product in products { - if product.id.hasSuffix(".annual") { - let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(12) / Float(shortestOptionPrice.0) - let discount = Int32(round((1.0 - fraction) * 20.0) * 5.0) - if discount > 0 { - if suggestions.contains(.restorePremium) { - return .premiumRestore(discount: discount) - } else if suggestions.contains(.annualPremium) { - return .premiumAnnualDiscount(discount: discount) - } else if suggestions.contains(.upgradePremium) { - return .premiumUpgrade(discount: discount) - } - } - break - } - } - - return nil - } else { - return nil - } - } - } else { - return .single(nil) + if notSet { + return .single(.setupPassword) } } - ) + if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager { + return inAppPurchaseManager.availableProducts + |> map { products -> ChatListNotice? in + if products.count > 1 { + let shortestOptionPrice: (Int64, NSDecimalNumber) + if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) { + shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) + } else { + shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) + } + for product in products { + if product.id.hasSuffix(".annual") { + let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(12) / Float(shortestOptionPrice.0) + let discount = Int32(round((1.0 - fraction) * 20.0) * 5.0) + if discount > 0 { + if suggestions.contains(.restorePremium) { + return .premiumRestore(discount: discount) + } else if suggestions.contains(.annualPremium) { + return .premiumAnnualDiscount(discount: discount) + } else if suggestions.contains(.upgradePremium) { + return .premiumUpgrade(discount: discount) + } + } + break + } + } + + return nil + } else { + return nil + } + } + } else { + return .single(nil) + } + } |> distinctUntilChanged - } else { - suggestedChatListNotice = .single(nil) - } + + self.suggestedChatListNotice.set(suggestedChatListNoticeSignal) + }) let storageInfo: Signal if !"".isEmpty, case .chatList(groupId: .root) = location, chatListFilter == nil { @@ -2008,7 +2022,7 @@ public final class ChatListNode: ListView { hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, - suggestedChatListNotice, + suggestedChatListNotice.get(), savedMessagesPeer, chatListViewUpdate, self.statePromise.get(), @@ -2028,7 +2042,9 @@ public final class ChatListNode: ListView { notice = nil } - let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: isMainTab) + let innerIsMainTab = location == .chatList(groupId: .root) && chatListFilter == nil + + let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: innerIsMainTab) var isEmpty = true var entries = rawEntries.filter { entry in switch entry { @@ -2983,6 +2999,7 @@ public final class ChatListNode: ListView { self.updatedFilterDisposable.dispose() self.pollFilterUpdatesDisposable?.dispose() self.chatFilterUpdatesDisposable?.dispose() + self.updateIsMainTabDisposable?.dispose() } func updateFilter(_ filter: ChatListFilter?) { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index fccc9ab744..8c0f2bd8de 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -85,7 +85,7 @@ enum ChatListNotice: Equatable { case premiumUpgrade(discount: Int32) case premiumAnnualDiscount(discount: Int32) case premiumRestore(discount: Int32) - case reviewLogin(newSessionReview: NewSessionReview) + case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift index 3f7e03049a..7b2947a44e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListStorageInfoItem.swift @@ -203,11 +203,15 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode { titleString = titleStringValue textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) - case let .reviewLogin(newSessionReview): + case let .reviewLogin(newSessionReview, totalCount): spacing = 2.0 alignment = .center - let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelTitle, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) + var rawTitleString = item.strings.ChatList_SessionReview_PanelTitle + if totalCount > 1 { + rawTitleString = "1/\(totalCount) \(rawTitleString)" + } + let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)) titleString = titleStringValue textString = NSAttributedString(string: item.strings.ChatList_SessionReview_PanelText(newSessionReview.device, newSessionReview.location).string, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 763b1630a3..920c8be0c3 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -18,6 +18,34 @@ import FastBlur import MediaEditor import RadialStatusNode +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! +}() + enum MediaPickerGridItemContent: Equatable { case asset(PHFetchResult, Int) case media(MediaPickerScreen.Subject.Media, Int) @@ -78,19 +106,6 @@ final class MediaPickerGridItem: GridItem { } } -private let maskImage = generateImage(CGSize(width: 1.0, height: 36.0), opaque: false, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.45).cgColor] as CFArray - - var locations: [CGFloat] = [0.0, 1.0] - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) -}) - final class MediaPickerGridItemNode: GridItemNode { var currentMediaState: (TGMediaSelectableItem, Int)? var currentAssetState: (PHFetchResult, Int)? @@ -104,7 +119,8 @@ final class MediaPickerGridItemNode: GridItemNode { private let backgroundNode: ASImageNode private let imageNode: ImageNode private var checkNode: InteractiveCheckNode? - private let gradientNode: ASImageNode + private let leftShadowNode: ASImageNode + private let rightShadowNode: ASImageNode private let typeIconNode: ASImageNode private let durationNode: ImmediateTextNode private let draftNode: ImmediateTextNode @@ -135,11 +151,17 @@ final class MediaPickerGridItemNode: GridItemNode { self.imageNode.isLayerBacked = true self.imageNode.animateFirstTransition = false - self.gradientNode = ASImageNode() - self.gradientNode.displaysAsynchronously = false - self.gradientNode.displayWithoutProcessing = true - self.gradientNode.image = maskImage - self.gradientNode.isLayerBacked = true + self.leftShadowNode = ASImageNode() + self.leftShadowNode.displaysAsynchronously = false + self.leftShadowNode.displayWithoutProcessing = true + self.leftShadowNode.image = leftShadowImage + self.leftShadowNode.isLayerBacked = true + + self.rightShadowNode = ASImageNode() + self.rightShadowNode.displaysAsynchronously = false + self.rightShadowNode.displayWithoutProcessing = true + self.rightShadowNode.image = rightShadowImage + self.rightShadowNode.isLayerBacked = true self.typeIconNode = ASImageNode() self.typeIconNode.displaysAsynchronously = false @@ -148,6 +170,8 @@ final class MediaPickerGridItemNode: GridItemNode { self.durationNode = ImmediateTextNode() self.durationNode.isLayerBacked = true + self.durationNode.textShadowColor = UIColor(white: 0.0, alpha: 0.4) + self.durationNode.textShadowBlur = 4.0 self.draftNode = ImmediateTextNode() self.activateAreaNode = AccessibilityAreaNode() @@ -248,7 +272,8 @@ final class MediaPickerGridItemNode: GridItemNode { if animateCheckNode { self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } - self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.leftShadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.rightShadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.draftNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) @@ -322,10 +347,10 @@ final class MediaPickerGridItemNode: GridItemNode { if draft.isVideo { self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo") - self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(draft.duration ?? 0.0)), font: Font.semibold(12.0), textColor: .white) + self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(draft.duration ?? 0.0)), font: Font.semibold(11.0), textColor: .white) if self.typeIconNode.supernode == nil { - self.addSubnode(self.gradientNode) + self.addSubnode(self.rightShadowNode) self.addSubnode(self.typeIconNode) self.addSubnode(self.durationNode) self.setNeedsLayout() @@ -337,8 +362,11 @@ final class MediaPickerGridItemNode: GridItemNode { if self.durationNode.supernode != nil { self.durationNode.removeFromSupernode() } - if self.gradientNode.supernode != nil { - self.gradientNode.removeFromSupernode() + if self.leftShadowNode.supernode != nil { + self.leftShadowNode.removeFromSupernode() + } + if self.rightShadowNode.supernode != nil { + self.rightShadowNode.removeFromSupernode() } } @@ -537,12 +565,20 @@ final class MediaPickerGridItemNode: GridItemNode { duration = stringForDuration(Int32(asset.duration)) } - if typeIcon != nil || duration != nil { - if self.gradientNode.supernode == nil { - self.addSubnode(self.gradientNode) + if typeIcon != nil { + if self.leftShadowNode.supernode == nil { + self.addSubnode(self.leftShadowNode) } - } else if self.gradientNode.supernode != nil { - self.gradientNode.removeFromSupernode() + } else if self.leftShadowNode.supernode != nil { + self.leftShadowNode.removeFromSupernode() + } + + if duration != nil { + if self.rightShadowNode.supernode == nil { + self.addSubnode(self.rightShadowNode) + } + } else if self.rightShadowNode.supernode != nil { + self.rightShadowNode.removeFromSupernode() } if let typeIcon { @@ -555,7 +591,7 @@ final class MediaPickerGridItemNode: GridItemNode { } if let duration { - self.durationNode.attributedText = NSAttributedString(string: duration, font: Font.semibold(12.0), textColor: .white) + self.durationNode.attributedText = NSAttributedString(string: duration, font: Font.semibold(11.0), textColor: .white) if self.durationNode.supernode == nil { self.addSubnode(self.durationNode) } @@ -608,13 +644,14 @@ final class MediaPickerGridItemNode: GridItemNode { let backgroundSize = CGSize(width: self.bounds.width, height: floorToScreenPixels(self.bounds.height / 9.0 * 16.0)) self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.bounds.height - backgroundSize.height) / 2.0)), size: backgroundSize) self.imageNode.frame = self.bounds - self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 36.0, width: self.bounds.width, height: 36.0) + self.leftShadowNode.frame = CGRect(x: 0.0, y: self.bounds.height - leftShadowImage.size.height, width: min(leftShadowImage.size.width, self.bounds.width), height: leftShadowImage.size.height) + self.rightShadowNode.frame = CGRect(x: self.bounds.width - min(rightShadowImage.size.width, self.bounds.width), y: self.bounds.height - rightShadowImage.size.height, width: min(rightShadowImage.size.width, self.bounds.width), height: rightShadowImage.size.height) self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0) self.activateAreaNode.frame = self.bounds if self.durationNode.supernode != nil { let durationSize = self.durationNode.updateLayout(self.bounds.size) - self.durationNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - durationSize.width - 7.0, y: self.bounds.height - durationSize.height - 5.0), size: durationSize) + self.durationNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - durationSize.width - 6.0, y: self.bounds.height - durationSize.height - 6.0), size: durationSize) } if self.draftNode.supernode != nil { diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index 8b6fbe3436..a562931daa 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -34,6 +34,7 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/GZip:GZip", "//submodules/ShimmerEffect:ShimmerEffect", + "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", ], visibility = [ "//visibility:public", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index e9b261aa05..709f848634 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -13,6 +13,7 @@ import AccountContext import AnimationCache import MultiAnimationRenderer import ShimmerEffect +import GenerateStickerPlaceholderImage private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in diff --git a/submodules/ShimmerEffect/BUILD b/submodules/ShimmerEffect/BUILD index 0943d7e93d..5c632871c5 100644 --- a/submodules/ShimmerEffect/BUILD +++ b/submodules/ShimmerEffect/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", + "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", ], visibility = [ "//visibility:public", diff --git a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift index a9e45c3f70..0fd541b28d 100644 --- a/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift +++ b/submodules/ShimmerEffect/Sources/StickerShimmerEffectNode.swift @@ -1,57 +1,7 @@ import Foundation import AsyncDisplayKit import Display - -private let decodingMap: [String] = ["A", "A", "C", "A", "A", "A", "A", "H", "A", "A", "A", "L", "M", "A", "A", "A", "Q", "A", "S", "T", "A", "V", "A", "A", "A", "Z", "a", "a", "c", "a", "a", "a", "a", "h", "a", "a", "a", "l", "m", "a", "a", "a", "q", "a", "s", "t", "a", "v", "a", ".", "a", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", ","] -private func decodeStickerThumbnailData(_ data: Data) -> String { - var string = "M" - data.forEach { byte in - if byte >= 128 + 64 { - string.append(decodingMap[Int(byte) - 128 - 64]) - } else { - if byte >= 128 { - string.append(",") - } else if byte >= 64 { - string.append("-") - } - string.append("\(byte & 63)") - } - } - string.append("z") - return string -} - -public func generateStickerPlaceholderImage(data: Data?, size: CGSize, scale: CGFloat? = nil, imageSize: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor) -> UIImage? { - return generateImage(size, scale: scale, rotatedContext: { size, context in - if let backgroundColor = backgroundColor { - context.setFillColor(backgroundColor.cgColor) - context.setBlendMode(.copy) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor.clear.cgColor) - } else { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(foregroundColor.cgColor) - } - - if let data = data { - var path = decodeStickerThumbnailData(data) - if !path.hasSuffix("z") { - path = "\(path)z" - } - let reader = PathDataReader(input: path) - let segments = reader.read() - - let scale = max(size.width, size.height) / max(imageSize.width, imageSize.height) - context.scaleBy(x: scale, y: scale) - renderPath(segments, context: context) - } else { - let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10.0, height: 10.0)) - UIGraphicsPushContext(context) - path.fill() - UIGraphicsPopContext() - } - }) -} +import GenerateStickerPlaceholderImage public class StickerShimmerEffectNode: ASDisplayNode { private var backdropNode: ASDisplayNode? @@ -155,512 +105,3 @@ public class StickerShimmerEffectNode: ASDisplayNode { self.effectNode.frame = bounds } } - -open class PathSegment: Equatable { - public enum SegmentType { - case M - case L - case C - case Q - case A - case z - case H - case V - case S - case T - case m - case l - case c - case q - case a - case h - case v - case s - case t - case E - case e - } - - public let type: SegmentType - public let data: [Double] - - public init(type: PathSegment.SegmentType = .M, data: [Double] = []) { - self.type = type - self.data = data - } - - open func isAbsolute() -> Bool { - switch type { - case .M, .L, .H, .V, .C, .S, .Q, .T, .A, .E: - return true - default: - return false - } - } - - public static func == (lhs: PathSegment, rhs: PathSegment) -> Bool { - return lhs.type == rhs.type && lhs.data == rhs.data - } -} - -private func renderPath(_ segments: [PathSegment], context: CGContext) { - var currentPoint: CGPoint? - var cubicPoint: CGPoint? - var quadrPoint: CGPoint? - var initialPoint: CGPoint? - - func M(_ x: Double, y: Double) { - let point = CGPoint(x: CGFloat(x), y: CGFloat(y)) - context.move(to: point) - setInitPoint(point) - } - - func m(_ x: Double, y: Double) { - if let cur = currentPoint { - let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) - context.move(to: next) - setInitPoint(next) - } else { - M(x, y: y) - } - } - - func L(_ x: Double, y: Double) { - lineTo(CGPoint(x: CGFloat(x), y: CGFloat(y))) - } - - func l(_ x: Double, y: Double) { - if let cur = currentPoint { - lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)) - } else { - L(x, y: y) - } - } - - func H(_ x: Double) { - if let cur = currentPoint { - lineTo(CGPoint(x: CGFloat(x), y: CGFloat(cur.y))) - } - } - - func h(_ x: Double) { - if let cur = currentPoint { - lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(cur.y))) - } - } - - func V(_ y: Double) { - if let cur = currentPoint { - lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y))) - } - } - - func v(_ y: Double) { - if let cur = currentPoint { - lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y) + cur.y)) - } - } - - func lineTo(_ p: CGPoint) { - context.addLine(to: p) - setPoint(p) - } - - func c(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { - if let cur = currentPoint { - let endPoint = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) - let controlPoint1 = CGPoint(x: CGFloat(x1) + cur.x, y: CGFloat(y1) + cur.y) - let controlPoint2 = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) - context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) - setCubicPoint(endPoint, cubic: controlPoint2) - } - } - - func C(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { - let endPoint = CGPoint(x: CGFloat(x), y: CGFloat(y)) - let controlPoint1 = CGPoint(x: CGFloat(x1), y: CGFloat(y1)) - let controlPoint2 = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) - context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) - setCubicPoint(endPoint, cubic: controlPoint2) - } - - func s(_ x2: Double, y2: Double, x: Double, y: Double) { - if let cur = currentPoint { - let nextCubic = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) - let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) - - let xy1: CGPoint - if let curCubicVal = cubicPoint { - xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) - } else { - xy1 = cur - } - context.addCurve(to: next, control1: xy1, control2: nextCubic) - setCubicPoint(next, cubic: nextCubic) - } - } - - func S(_ x2: Double, y2: Double, x: Double, y: Double) { - if let cur = currentPoint { - let nextCubic = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) - let next = CGPoint(x: CGFloat(x), y: CGFloat(y)) - let xy1: CGPoint - if let curCubicVal = cubicPoint { - xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) - } else { - xy1 = cur - } - context.addCurve(to: next, control1: xy1, control2: nextCubic) - setCubicPoint(next, cubic: nextCubic) - } - } - - func z() { - context.fillPath() - } - - func setQuadrPoint(_ p: CGPoint, quadr: CGPoint) { - currentPoint = p - quadrPoint = quadr - cubicPoint = nil - } - - func setCubicPoint(_ p: CGPoint, cubic: CGPoint) { - currentPoint = p - cubicPoint = cubic - quadrPoint = nil - } - - func setInitPoint(_ p: CGPoint) { - setPoint(p) - initialPoint = p - } - - func setPoint(_ p: CGPoint) { - currentPoint = p - cubicPoint = nil - quadrPoint = nil - } - - let _ = initialPoint - let _ = quadrPoint - - for segment in segments { - var data = segment.data - switch segment.type { - case .M: - M(data[0], y: data[1]) - data.removeSubrange(Range(uncheckedBounds: (lower: 0, upper: 2))) - while data.count >= 2 { - L(data[0], y: data[1]) - data.removeSubrange((0 ..< 2)) - } - case .m: - m(data[0], y: data[1]) - data.removeSubrange((0 ..< 2)) - while data.count >= 2 { - l(data[0], y: data[1]) - data.removeSubrange((0 ..< 2)) - } - case .L: - while data.count >= 2 { - L(data[0], y: data[1]) - data.removeSubrange((0 ..< 2)) - } - case .l: - while data.count >= 2 { - l(data[0], y: data[1]) - data.removeSubrange((0 ..< 2)) - } - case .H: - H(data[0]) - case .h: - h(data[0]) - case .V: - V(data[0]) - case .v: - v(data[0]) - case .C: - while data.count >= 6 { - C(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) - data.removeSubrange((0 ..< 6)) - } - case .c: - while data.count >= 6 { - c(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) - data.removeSubrange((0 ..< 6)) - } - case .S: - while data.count >= 4 { - S(data[0], y2: data[1], x: data[2], y: data[3]) - data.removeSubrange((0 ..< 4)) - } - case .s: - while data.count >= 4 { - s(data[0], y2: data[1], x: data[2], y: data[3]) - data.removeSubrange((0 ..< 4)) - } - case .z: - z() - default: - print("unknown") - break - } - } -} - -private class PathDataReader { - private let input: String - private var current: UnicodeScalar? - private var previous: UnicodeScalar? - private var iterator: String.UnicodeScalarView.Iterator - - private static let spaces: Set = Set("\n\r\t ,".unicodeScalars) - - init(input: String) { - self.input = input - self.iterator = input.unicodeScalars.makeIterator() - } - - public func read() -> [PathSegment] { - readNext() - var segments = [PathSegment]() - while let array = readSegments() { - segments.append(contentsOf: array) - } - return segments - } - - private func readSegments() -> [PathSegment]? { - if let type = readSegmentType() { - let argCount = getArgCount(segment: type) - if argCount == 0 { - return [PathSegment(type: type)] - } - var result = [PathSegment]() - let data: [Double] - if type == .a || type == .A { - data = readDataOfASegment() - } else { - data = readData() - } - var index = 0 - var isFirstSegment = true - while index < data.count { - let end = index + argCount - if end > data.count { - break - } - var currentType = type - if type == .M && !isFirstSegment { - currentType = .L - } - if type == .m && !isFirstSegment { - currentType = .l - } - result.append(PathSegment(type: currentType, data: Array(data[index.. [Double] { - var data = [Double]() - while true { - skipSpaces() - if let value = readNum() { - data.append(value) - } else { - return data - } - } - } - - private func readDataOfASegment() -> [Double] { - let argCount = getArgCount(segment: .A) - var data: [Double] = [] - var index = 0 - while true { - skipSpaces() - let value: Double? - let indexMod = index % argCount - if indexMod == 3 || indexMod == 4 { - value = readFlag() - } else { - value = readNum() - } - guard let doubleValue = value else { - return data - } - data.append(doubleValue) - index += 1 - } - return data - } - - private func skipSpaces() { - var currentCharacter = current - while let character = currentCharacter, Self.spaces.contains(character) { - currentCharacter = readNext() - } - } - - private func readFlag() -> Double? { - guard let ch = current else { - return .none - } - readNext() - switch ch { - case "0": - return 0 - case "1": - return 1 - default: - return .none - } - } - - fileprivate func readNum() -> Double? { - guard let ch = current else { - return .none - } - - guard ch >= "0" && ch <= "9" || ch == "." || ch == "-" else { - return .none - } - - var chars = [ch] - var hasDot = ch == "." - while let ch = readDigit(&hasDot) { - chars.append(ch) - } - - var buf = "" - buf.unicodeScalars.append(contentsOf: chars) - guard let value = Double(buf) else { - return .none - } - return value - } - - fileprivate func readDigit(_ hasDot: inout Bool) -> UnicodeScalar? { - if let ch = readNext() { - if (ch >= "0" && ch <= "9") || ch == "e" || (previous == "e" && ch == "-") { - return ch - } else if ch == "." && !hasDot { - hasDot = true - return ch - } - } - return nil - } - - fileprivate func isNum(ch: UnicodeScalar, hasDot: inout Bool) -> Bool { - switch ch { - case "0"..."9": - return true - case ".": - if hasDot { - return false - } - hasDot = true - default: - return true - } - return false - } - - @discardableResult - private func readNext() -> UnicodeScalar? { - previous = current - current = iterator.next() - return current - } - - private func isAcceptableSeparator(_ ch: UnicodeScalar?) -> Bool { - if let ch = ch { - return "\n\r\t ,".contains(String(ch)) - } - return false - } - - private func readSegmentType() -> PathSegment.SegmentType? { - while true { - if let type = getPathSegmentType() { - readNext() - return type - } - if readNext() == nil { - return nil - } - } - } - - fileprivate func getPathSegmentType() -> PathSegment.SegmentType? { - if let ch = current { - switch ch { - case "M": - return .M - case "m": - return .m - case "L": - return .L - case "l": - return .l - case "C": - return .C - case "c": - return .c - case "Q": - return .Q - case "q": - return .q - case "A": - return .A - case "a": - return .a - case "z", "Z": - return .z - case "H": - return .H - case "h": - return .h - case "V": - return .V - case "v": - return .v - case "S": - return .S - case "s": - return .s - case "T": - return .T - case "t": - return .t - default: - break - } - } - return nil - } - - fileprivate func getArgCount(segment: PathSegment.SegmentType) -> Int { - switch segment { - case .H, .h, .V, .v: - return 1 - case .M, .m, .L, .l, .T, .t: - return 2 - case .S, .s, .Q, .q: - return 4 - case .C, .c: - return 6 - case .A, .a: - return 7 - default: - return 0 - } - } -} diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 92cab8ba35..64c5015b0a 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1276,7 +1276,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { animationData: animationData, content: .animation(animationData), itemFile: itemFile, subgroupId: nil, - icon: .none, + icon: itemFile.isPremiumSticker ? .premium : .none, tintMode: animationData.isTemplate ? .primary : .none ) items.append(item) diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD index 225ce0e5a6..bff8f46635 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/TelegramUIPreferences", + "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 1416d25a2a..8c4bdd3667 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -16,6 +16,7 @@ import MultiAnimationRenderer import ShimmerEffect import TextFormat import TelegramUIPreferences +import GenerateStickerPlaceholderImage public func generateTopicIcon(title: String, backgroundColors: [UIColor], strokeColors: [UIColor], size: CGSize) -> UIImage? { let realSize = size diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 07f7dfcf90..a08c0ef49f 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -46,6 +46,7 @@ swift_library( "//submodules/GZip", "//submodules/rlottie:RLottieBinding", "//submodules/lottie-ios:Lottie", + "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index f0df441cb2..614b0ee31d 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -24,6 +24,7 @@ import SolidRoundedButtonComponent import EmojiTextAttachmentView import EmojiStatusComponent import TelegramNotices +import GenerateStickerPlaceholderImage private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift index 4ca1c3f602..8d35cb2bf3 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift @@ -43,10 +43,10 @@ private final class LottieDirectContent: LottieComponent.Content { return true } - override func load(_ f: @escaping (Data, String?) -> Void) -> Disposable { + override func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.path)) { let result = TGGUnzipData(data, 2 * 1024 * 1024) ?? data - f(result, nil) + f(.animation(data: result, cacheKey: nil)) } return EmptyDisposable diff --git a/submodules/TelegramUI/Components/LottieComponent/BUILD b/submodules/TelegramUI/Components/LottieComponent/BUILD index f6b690b81c..bf53f8f792 100644 --- a/submodules/TelegramUI/Components/LottieComponent/BUILD +++ b/submodules/TelegramUI/Components/LottieComponent/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AppBundle", "//submodules/GZip", + "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index 5bfc68e1e4..e9ee252dc8 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -7,10 +7,16 @@ import RLottieBinding import SwiftSignalKit import AppBundle import GZip +import GenerateStickerPlaceholderImage public final class LottieComponent: Component { public typealias EnvironmentType = Empty + public enum ContentData { + case placeholder(data: Data) + case animation(data: Data, cacheKey: String?) + } + open class Content: Equatable { open var frameRange: Range { preconditionFailure() @@ -30,7 +36,7 @@ public final class LottieComponent: Component { preconditionFailure() } - open func load(_ f: @escaping (Data, String?) -> Void) -> Disposable { + open func load(_ f: @escaping (ContentData) -> Void) -> Disposable { preconditionFailure() } } @@ -61,11 +67,11 @@ public final class LottieComponent: Component { return true } - override public func load(_ f: @escaping (Data, String?) -> Void) -> Disposable { + override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable { if let url = getAppBundle().url(forResource: self.name, withExtension: "json"), let data = try? Data(contentsOf: url) { - f(data, url.path) + f(.animation(data: data, cacheKey: url.path)) } else if let url = getAppBundle().url(forResource: self.name, withExtension: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) { - f(unpackedData, url.path) + f(.animation(data: unpackedData, cacheKey: url.path)) } return EmptyDisposable @@ -80,6 +86,7 @@ public final class LottieComponent: Component { public let content: Content public let color: UIColor? + public let placeholderColor: UIColor? public let startingPosition: StartingPosition public let size: CGSize? public let renderingScale: CGFloat? @@ -88,6 +95,7 @@ public final class LottieComponent: Component { public init( content: Content, color: UIColor? = nil, + placeholderColor: UIColor? = nil, startingPosition: StartingPosition = .end, size: CGSize? = nil, renderingScale: CGFloat? = nil, @@ -95,6 +103,7 @@ public final class LottieComponent: Component { ) { self.content = content self.color = color + self.placeholderColor = placeholderColor self.startingPosition = startingPosition self.size = size self.renderingScale = renderingScale @@ -108,6 +117,9 @@ public final class LottieComponent: Component { if lhs.color != rhs.color { return false } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } if lhs.startingPosition != rhs.startingPosition { return false } @@ -272,6 +284,26 @@ public final class LottieComponent: Component { } } + private func loadPlaceholder(data: Data) { + guard let component = self.component, let placeholderColor = component.placeholderColor else { + return + } + guard let currentDisplaySize = self.currentDisplaySize else { + return + } + + if let image = generateStickerPlaceholderImage( + data: data, + size: currentDisplaySize, + scale: min(2.0, UIScreenScale), + imageSize: CGSize(width: 512.0, height: 512.0), + backgroundColor: nil, + foregroundColor: placeholderColor + ) { + self.image = image + } + } + private func loadAnimation(data: Data, cacheKey: String?, startingPosition: StartingPosition, frameRange: Range) { self.animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: cacheKey ?? "") if let animationInstance = self.animationInstance { @@ -395,12 +427,17 @@ public final class LottieComponent: Component { self.currentContentDisposable?.dispose() let content = component.content let frameRange = content.frameRange - self.currentContentDisposable = component.content.load { [weak self, weak content] data, cacheKey in + self.currentContentDisposable = component.content.load { [weak self, weak content] result in Queue.mainQueue().async { guard let self, let component = self.component, component.content == content else { return } - self.loadAnimation(data: data, cacheKey: cacheKey, startingPosition: component.startingPosition, frameRange: frameRange) + switch result { + case let .placeholder(data): + self.loadPlaceholder(data: data) + case let .animation(data, cacheKey): + self.loadAnimation(data: data, cacheKey: cacheKey, startingPosition: component.startingPosition, frameRange: frameRange) + } } } } else if redrawImage { diff --git a/submodules/TelegramUI/Components/LottieComponentEmojiContent/Sources/LottieComponentEmojiContent.swift b/submodules/TelegramUI/Components/LottieComponentEmojiContent/Sources/LottieComponentEmojiContent.swift index 6fcacb6b66..ff0fefd7fd 100644 --- a/submodules/TelegramUI/Components/LottieComponentEmojiContent/Sources/LottieComponentEmojiContent.swift +++ b/submodules/TelegramUI/Components/LottieComponentEmojiContent/Sources/LottieComponentEmojiContent.swift @@ -34,7 +34,7 @@ public extension LottieComponent { return true } - override public func load(_ f: @escaping (Data, String?) -> Void) -> Disposable { + override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable { let fileId = self.fileId let mediaBox = self.context.account.postbox.mediaBox return (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) @@ -64,7 +64,7 @@ public extension LottieComponent { guard let data else { return } - f(data, nil) + f(.animation(data: data, cacheKey: nil)) }) } } diff --git a/submodules/TelegramUI/Components/LottieComponentResourceContent/Sources/LottieComponentResourceContent.swift b/submodules/TelegramUI/Components/LottieComponentResourceContent/Sources/LottieComponentResourceContent.swift index 22ea9206ea..cf3127ed61 100644 --- a/submodules/TelegramUI/Components/LottieComponentResourceContent/Sources/LottieComponentResourceContent.swift +++ b/submodules/TelegramUI/Components/LottieComponentResourceContent/Sources/LottieComponentResourceContent.swift @@ -10,6 +10,7 @@ public extension LottieComponent { private let context: AccountContext private let file: TelegramMediaFile private let attemptSynchronously: Bool + private let providesPlaceholder: Bool override public var frameRange: Range { return 0.0 ..< 1.0 @@ -18,11 +19,13 @@ public extension LottieComponent { public init( context: AccountContext, file: TelegramMediaFile, - attemptSynchronously: Bool + attemptSynchronously: Bool, + providesPlaceholder: Bool = false ) { self.context = context self.file = file self.attemptSynchronously = attemptSynchronously + self.providesPlaceholder = providesPlaceholder super.init() } @@ -37,13 +40,17 @@ public extension LottieComponent { if self.attemptSynchronously != other.attemptSynchronously { return false } + if self.providesPlaceholder != other.providesPlaceholder { + return false + } return true } - override public func load(_ f: @escaping (Data, String?) -> Void) -> Disposable { + override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable { let attemptSynchronously = self.attemptSynchronously let file = self.file let mediaBox = self.context.account.postbox.mediaBox + let providesPlaceholder = self.providesPlaceholder return Signal { subscriber in if attemptSynchronously { if let path = mediaBox.completedResourcePath(file.resource), let contents = try? Data(contentsOf: URL(fileURLWithPath: path)) { @@ -56,11 +63,15 @@ public extension LottieComponent { } let dataDisposable = (mediaBox.resourceData(file.resource) - |> filter { data in return data.complete }).start(next: { data in - if let contents = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { - let result = TGGUnzipData(contents, 2 * 1024 * 1024) ?? contents - subscriber.putNext(result) - subscriber.putCompletion() + ).start(next: { data in + if data.complete { + if let contents = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + let result = TGGUnzipData(contents, 2 * 1024 * 1024) ?? contents + subscriber.putNext(result) + subscriber.putCompletion() + } else { + subscriber.putNext(nil) + } } else { subscriber.putNext(nil) } @@ -73,9 +84,12 @@ public extension LottieComponent { } }.start(next: { data in guard let data else { + if providesPlaceholder, let immediateThumbnailData = file.immediateThumbnailData { + f(.placeholder(data: immediateThumbnailData)) + } return } - f(data, nil) + f(.animation(data: data, cacheKey: nil)) }) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 11f8e2c312..05105325db 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -50,7 +50,10 @@ public final class MediaEditor { private var player: AVPlayer? private var additionalPlayer: AVPlayer? private var audioPlayer: AVPlayer? + private var timeObserver: Any? + private weak var timeObserverPlayer: AVPlayer? + private var didPlayToEndTimeObserver: NSObjectProtocol? private weak var previewView: MediaEditorPreviewView? @@ -354,12 +357,10 @@ public final class MediaEditor { private func destroyTimeObservers() { if let timeObserver = self.timeObserver { - if self.sourceIsVideo { - self.player?.removeTimeObserver(timeObserver) - } else { - self.audioPlayer?.removeTimeObserver(timeObserver) - } + self.timeObserverPlayer?.removeTimeObserver(timeObserver) + self.timeObserver = nil + self.timeObserverPlayer = nil } if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver { NotificationCenter.default.removeObserver(didPlayToEndTimeObserver) @@ -616,6 +617,7 @@ public final class MediaEditor { } if self.timeObserver == nil { + self.timeObserverPlayer = observedPlayer self.timeObserver = observedPlayer.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self, weak observedPlayer] time in guard let self, let observedPlayer, let duration = observedPlayer.currentItem?.duration.seconds else { return diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 5946082bb3..556ecd5ab9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -129,7 +129,10 @@ private struct Month: Equatable { } } -private let durationFont = Font.regular(12.0) +private let durationFont: UIFont = { + Font.semibold(11.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)) @@ -144,6 +147,46 @@ private let minDurationImage: UIImage = { 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 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 final class DurationLayer: CALayer { override init() { super.init() @@ -159,6 +202,42 @@ private final class DurationLayer: CALayer { 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 { @@ -171,14 +250,11 @@ private final class DurationLayer: CALayer { 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.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor) - context.setBlendMode(.copy) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) - context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) - + 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() @@ -198,7 +274,7 @@ private protocol ItemLayer: SparseItemGridLayer { var hasContents: Bool { get set } func setSpoilerContents(_ contents: Any?) - func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) + func updateDuration(viewCount: Int32?, duration: Int32?, isMin: Bool, minFactor: CGFloat) func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) func updateHasSpoiler(hasSpoiler: Bool) @@ -208,7 +284,10 @@ private protocol ItemLayer: SparseItemGridLayer { private final class GenericItemLayer: CALayer, ItemLayer { var item: VisualMediaItem? + var viewCountLayer: DurationLayer? var durationLayer: DurationLayer? + var leftShadowLayer: SimpleLayer? + var rightShadowLayer: SimpleLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? var dustLayer: MediaDustLayer? @@ -254,17 +333,34 @@ private final class GenericItemLayer: CALayer, ItemLayer { self.item = item } - func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) { + func updateDuration(viewCount: Int32?, duration: Int32?, isMin: Bool, minFactor: CGFloat) { 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 = duration { + 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 - 3.0), size: CGSize()) + 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 } @@ -272,6 +368,40 @@ private final class GenericItemLayer: CALayer, ItemLayer { self.durationLayer = nil durationLayer.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() + } + } } func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) { @@ -330,8 +460,21 @@ private final class GenericItemLayer: CALayer, ItemLayer { } 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 - 3.0), size: CGSize()) + durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 4.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 binding = binding as? SparseItemGridBindingImpl, let item = item as? VisualMediaItem, let previousItem = self.item, previousItem.story.media.id != item.story.media.id { @@ -342,7 +485,10 @@ private final class GenericItemLayer: CALayer, ItemLayer { private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemLayer { var item: VisualMediaItem? + var viewCountLayer: DurationLayer? var durationLayer: DurationLayer? + var leftShadowLayer: SimpleLayer? + var rightShadowLayer: SimpleLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? var dustLayer: MediaDustLayer? @@ -398,17 +544,34 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL self.item = item } - func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) { + func updateDuration(viewCount: Int32?, duration: Int32?, isMin: Bool, minFactor: CGFloat) { self.minFactor = minFactor - if let duration = duration { + 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 - 3.0), size: CGSize()) + 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 } @@ -416,6 +579,40 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL self.durationLayer = nil durationLayer.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() + } + } } func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) { @@ -474,8 +671,21 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL } 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 - 3.0), size: CGSize()) + durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 4.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) } } } @@ -483,7 +693,11 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL private final class ItemTransitionView: UIView { private weak var itemLayer: CALayer? private var copyDurationLayer: SimpleLayer? + private var copyViewCountLayer: SimpleLayer? + private var copyLeftShadowLayer: SimpleLayer? + private var copyRightShadowLayer: SimpleLayer? + private var viewCountLayerBottomLeftPosition: CGPoint? private var durationLayerBottomLeftPosition: CGPoint? init(itemLayer: CALayer?) { @@ -494,15 +708,57 @@ private final class ItemTransitionView: UIView { if let itemLayer { self.layer.contentsRect = itemLayer.contentsRect + var viewCountLayer: CALayer? var durationLayer: CALayer? + var leftShadowLayer: CALayer? + var rightShadowLayer: CALayer? if let itemLayer = itemLayer as? CaptureProtectedItemLayer { + viewCountLayer = itemLayer.viewCountLayer durationLayer = itemLayer.durationLayer self.layer.contents = itemLayer.getContents() - } else if let itemLayer = itemLayer as? ItemLayer { + } else if let itemLayer = itemLayer as? GenericItemLayer { + viewCountLayer = itemLayer.viewCountLayer durationLayer = itemLayer.durationLayer + leftShadowLayer = itemLayer.leftShadowLayer + rightShadowLayer = itemLayer.rightShadowLayer 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 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 durationLayer { let copyDurationLayer = SimpleLayer() copyDurationLayer.contents = durationLayer.contents @@ -528,6 +784,18 @@ private final class ItemTransitionView: UIView { 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 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)) + } } } @@ -728,6 +996,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } } + var viewCount: Int32? + if let value = story.views?.seenCount { + viewCount = Int32(value) + } + var duration: Int32? var isMin: Bool = false if let file = selectedMedia as? TelegramMediaFile, !file.isAnimated { @@ -736,7 +1009,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding { } isMin = layer.bounds.width < 80.0 } - layer.updateDuration(duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0)) + layer.updateDuration(viewCount: viewCount, duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0)) } var isSelected: Bool? diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index f221745346..9cd478df32 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -275,7 +275,7 @@ private struct Month: Equatable { } } -private let durationFont = Font.regular(12.0) +private let durationFont = Font.semibold(11.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)) @@ -290,6 +290,18 @@ private let minDurationImage: UIImage = { 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 final class DurationLayer: CALayer { override init() { super.init() @@ -317,14 +329,11 @@ private final class DurationLayer: CALayer { 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.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor) - context.setBlendMode(.copy) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) - context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) - + 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() @@ -355,6 +364,7 @@ private protocol ItemLayer: SparseItemGridLayer { private final class GenericItemLayer: CALayer, ItemLayer { var item: VisualMediaItem? var durationLayer: DurationLayer? + var rightShadowLayer: SimpleLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? var dustLayer: MediaDustLayer? @@ -403,7 +413,7 @@ private final class GenericItemLayer: CALayer, ItemLayer { func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) { self.minFactor = minFactor - if let duration = duration { + if let duration { if let durationLayer = self.durationLayer { durationLayer.update(duration: duration, isMin: isMin) } else { @@ -418,6 +428,24 @@ private final class GenericItemLayer: CALayer, ItemLayer { self.durationLayer = nil durationLayer.removeFromSuperlayer() } + + let size = self.bounds.size + + 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() + } + } } func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) { @@ -476,15 +504,21 @@ private final class GenericItemLayer: CALayer, ItemLayer { } func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { - /*if let durationLayer = self.durationLayer { + if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) - }*/ + } + + 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) + } } } private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemLayer { var item: VisualMediaItem? var durationLayer: DurationLayer? + var rightShadowLayer: SimpleLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? var dustLayer: MediaDustLayer? @@ -543,7 +577,7 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) { self.minFactor = minFactor - if let duration = duration { + if let duration { if let durationLayer = self.durationLayer { durationLayer.update(duration: duration, isMin: isMin) } else { @@ -558,6 +592,24 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL self.durationLayer = nil durationLayer.removeFromSuperlayer() } + + let size = self.bounds.size + + 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() + } + } } func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) { @@ -616,9 +668,14 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL } func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { - /*if let durationLayer = self.durationLayer { + if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) - }*/ + } + + 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) + } } } diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 24d3dfeeb8..1ab0330a14 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -212,7 +212,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.chatListNode = nil } else { self.mainContainerNode = nil - self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true) + self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) } super.init() diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index bd77d6434a..bfbe7676fa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -448,6 +448,16 @@ final class StoryContentCaptionComponent: Component { } } } + } else { + if case .tap = gesture { + if component.externalState.isSelectingText { + self.cancelTextSelection() + } else if self.isExpanded { + self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } else { + self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + } } } default: diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 07c5fe5170..0aeddf6959 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -779,7 +779,7 @@ final class StoryItemContentComponent: Component { if let current = self.loadingEffectView { loadingEffectView = current } else { - loadingEffectView = StoryItemLoadingEffectView(effectAlpha: 0.1, borderAlpha: 0.2, duration: 1.0, hasCustomBorder: true, playOnce: false) + loadingEffectView = StoryItemLoadingEffectView(effectAlpha: 0.1, borderAlpha: 0.2, duration: 1.0, hasCustomBorder: false, playOnce: false) self.loadingEffectView = loadingEffectView self.addSubview(loadingEffectView) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift index aa7e12031e..336353b6e9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift @@ -4,6 +4,10 @@ import HierarchyTrackingLayer import ComponentFlow import Display +private let shadowImage: UIImage? = { + UIImage(named: "Stories/PanelGradient") +}() + final class StoryItemLoadingEffectView: UIView { private let duration: Double private let hasCustomBorder: Bool @@ -46,16 +50,32 @@ final class StoryItemLoadingEffectView: UIView { let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in - let backgroundColor = UIColor.clear - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(backgroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: CGPoint(), size: size)) + let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0)) - let foregroundColor = UIColor(white: 1.0, alpha: baseAlpha) + if let shadowImage { + UIGraphicsPushContext(context) + + for i in 0 ..< 2 { + let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height)) + + context.saveGState() + context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY) + context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5) + let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width)) + + context.clip(to: adjustedRect, mask: shadowImage.cgImage!) + context.setFillColor(foregroundColor.cgColor) + context.fill(adjustedRect) + + context.restoreGState() + } + + UIGraphicsPopContext() + } + /* let numColors = 7 var locations: [CGFloat] = [] var colors: [CGColor] = [] @@ -72,7 +92,7 @@ final class StoryItemLoadingEffectView: UIView { let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())*/ }) } self.backgroundView.image = generateGradient(effectAlpha) @@ -89,6 +109,11 @@ final class StoryItemLoadingEffectView: UIView { } private func updateAnimations(size: CGSize) { + /*if "".isEmpty { + self.backgroundView.center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) + return + }*/ + if self.backgroundView.layer.animation(forKey: "shimmer") != nil || (self.playOnce && self.didPlayOnce) { return } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift index 088eba59ad..d675e2c45b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift @@ -415,11 +415,13 @@ final class StoryItemOverlaysView: UIView { if file.isCustomTemplateEmoji { color = flags.contains(.isDark) ? .white : .black } + let placeholderColor = flags.contains(.isDark) ? UIColor(white: 1.0, alpha: 0.1) : UIColor(white: 0.0, alpha: 0.1) let _ = directStickerView.update( transition: .immediate, component: AnyComponent(LottieComponent( - content: LottieComponent.ResourceContent(context: context, file: file, attemptSynchronously: synchronous), + content: LottieComponent.ResourceContent(context: context, file: file, attemptSynchronously: synchronous, providesPlaceholder: true), color: color, + placeholderColor: placeholderColor, renderingScale: 2.0, loop: true )), diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 24f6b9349c..15bf408e8c 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -901,21 +901,30 @@ public final class TextFieldComponent: Component { if self.textView.inputView == nil { self.textView.inputView = inputView if self.textView.isFirstResponder { - self.textView.reloadInputViews() + // Avoid layout cycle + DispatchQueue.main.async { [weak self] in + self?.textView.reloadInputViews() + } } } } else if component.hideKeyboard { if self.textView.inputView == nil { self.textView.inputView = EmptyInputView() if self.textView.isFirstResponder { - self.textView.reloadInputViews() + // Avoid layout cycle + DispatchQueue.main.async { [weak self] in + self?.textView.reloadInputViews() + } } } } else { if self.textView.inputView != nil { self.textView.inputView = nil if self.textView.isFirstResponder { - self.textView.reloadInputViews() + // Avoid layout cycle + DispatchQueue.main.async { [weak self] in + self?.textView.reloadInputViews() + } } } } diff --git a/submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage/BUILD b/submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage/BUILD new file mode 100644 index 0000000000..b388ccc50d --- /dev/null +++ b/submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GenerateStickerPlaceholderImage", + module_name = "GenerateStickerPlaceholderImage", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage/Sources/GenerateStickerPlaceholderImage.swift b/submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage/Sources/GenerateStickerPlaceholderImage.swift new file mode 100644 index 0000000000..71466fc8b1 --- /dev/null +++ b/submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage/Sources/GenerateStickerPlaceholderImage.swift @@ -0,0 +1,564 @@ +import Foundation +import UIKit +import Display + +open class PathSegment: Equatable { + public enum SegmentType { + case M + case L + case C + case Q + case A + case z + case H + case V + case S + case T + case m + case l + case c + case q + case a + case h + case v + case s + case t + case E + case e + } + + public let type: SegmentType + public let data: [Double] + + public init(type: PathSegment.SegmentType = .M, data: [Double] = []) { + self.type = type + self.data = data + } + + open func isAbsolute() -> Bool { + switch type { + case .M, .L, .H, .V, .C, .S, .Q, .T, .A, .E: + return true + default: + return false + } + } + + public static func == (lhs: PathSegment, rhs: PathSegment) -> Bool { + return lhs.type == rhs.type && lhs.data == rhs.data + } +} + +private func renderPath(_ segments: [PathSegment], context: CGContext) { + var currentPoint: CGPoint? + var cubicPoint: CGPoint? + var quadrPoint: CGPoint? + var initialPoint: CGPoint? + + func M(_ x: Double, y: Double) { + let point = CGPoint(x: CGFloat(x), y: CGFloat(y)) + context.move(to: point) + setInitPoint(point) + } + + func m(_ x: Double, y: Double) { + if let cur = currentPoint { + let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) + context.move(to: next) + setInitPoint(next) + } else { + M(x, y: y) + } + } + + func L(_ x: Double, y: Double) { + lineTo(CGPoint(x: CGFloat(x), y: CGFloat(y))) + } + + func l(_ x: Double, y: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)) + } else { + L(x, y: y) + } + } + + func H(_ x: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(x), y: CGFloat(cur.y))) + } + } + + func h(_ x: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(cur.y))) + } + } + + func V(_ y: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y))) + } + } + + func v(_ y: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y) + cur.y)) + } + } + + func lineTo(_ p: CGPoint) { + context.addLine(to: p) + setPoint(p) + } + + func c(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { + if let cur = currentPoint { + let endPoint = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) + let controlPoint1 = CGPoint(x: CGFloat(x1) + cur.x, y: CGFloat(y1) + cur.y) + let controlPoint2 = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) + context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) + setCubicPoint(endPoint, cubic: controlPoint2) + } + } + + func C(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { + let endPoint = CGPoint(x: CGFloat(x), y: CGFloat(y)) + let controlPoint1 = CGPoint(x: CGFloat(x1), y: CGFloat(y1)) + let controlPoint2 = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) + context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) + setCubicPoint(endPoint, cubic: controlPoint2) + } + + func s(_ x2: Double, y2: Double, x: Double, y: Double) { + if let cur = currentPoint { + let nextCubic = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) + let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) + + let xy1: CGPoint + if let curCubicVal = cubicPoint { + xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) + } else { + xy1 = cur + } + context.addCurve(to: next, control1: xy1, control2: nextCubic) + setCubicPoint(next, cubic: nextCubic) + } + } + + func S(_ x2: Double, y2: Double, x: Double, y: Double) { + if let cur = currentPoint { + let nextCubic = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) + let next = CGPoint(x: CGFloat(x), y: CGFloat(y)) + let xy1: CGPoint + if let curCubicVal = cubicPoint { + xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) + } else { + xy1 = cur + } + context.addCurve(to: next, control1: xy1, control2: nextCubic) + setCubicPoint(next, cubic: nextCubic) + } + } + + func z() { + context.fillPath() + } + + func setQuadrPoint(_ p: CGPoint, quadr: CGPoint) { + currentPoint = p + quadrPoint = quadr + cubicPoint = nil + } + + func setCubicPoint(_ p: CGPoint, cubic: CGPoint) { + currentPoint = p + cubicPoint = cubic + quadrPoint = nil + } + + func setInitPoint(_ p: CGPoint) { + setPoint(p) + initialPoint = p + } + + func setPoint(_ p: CGPoint) { + currentPoint = p + cubicPoint = nil + quadrPoint = nil + } + + let _ = initialPoint + let _ = quadrPoint + + for segment in segments { + var data = segment.data + switch segment.type { + case .M: + M(data[0], y: data[1]) + data.removeSubrange(Range(uncheckedBounds: (lower: 0, upper: 2))) + while data.count >= 2 { + L(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + } + case .m: + m(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + while data.count >= 2 { + l(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + } + case .L: + while data.count >= 2 { + L(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + } + case .l: + while data.count >= 2 { + l(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + } + case .H: + H(data[0]) + case .h: + h(data[0]) + case .V: + V(data[0]) + case .v: + v(data[0]) + case .C: + while data.count >= 6 { + C(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) + data.removeSubrange((0 ..< 6)) + } + case .c: + while data.count >= 6 { + c(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) + data.removeSubrange((0 ..< 6)) + } + case .S: + while data.count >= 4 { + S(data[0], y2: data[1], x: data[2], y: data[3]) + data.removeSubrange((0 ..< 4)) + } + case .s: + while data.count >= 4 { + s(data[0], y2: data[1], x: data[2], y: data[3]) + data.removeSubrange((0 ..< 4)) + } + case .z: + z() + default: + print("unknown") + break + } + } +} + +private class PathDataReader { + private let input: String + private var current: UnicodeScalar? + private var previous: UnicodeScalar? + private var iterator: String.UnicodeScalarView.Iterator + + private static let spaces: Set = Set("\n\r\t ,".unicodeScalars) + + init(input: String) { + self.input = input + self.iterator = input.unicodeScalars.makeIterator() + } + + public func read() -> [PathSegment] { + readNext() + var segments = [PathSegment]() + while let array = readSegments() { + segments.append(contentsOf: array) + } + return segments + } + + private func readSegments() -> [PathSegment]? { + if let type = readSegmentType() { + let argCount = getArgCount(segment: type) + if argCount == 0 { + return [PathSegment(type: type)] + } + var result = [PathSegment]() + let data: [Double] + if type == .a || type == .A { + data = readDataOfASegment() + } else { + data = readData() + } + var index = 0 + var isFirstSegment = true + while index < data.count { + let end = index + argCount + if end > data.count { + break + } + var currentType = type + if type == .M && !isFirstSegment { + currentType = .L + } + if type == .m && !isFirstSegment { + currentType = .l + } + result.append(PathSegment(type: currentType, data: Array(data[index.. [Double] { + var data = [Double]() + while true { + skipSpaces() + if let value = readNum() { + data.append(value) + } else { + return data + } + } + } + + private func readDataOfASegment() -> [Double] { + let argCount = getArgCount(segment: .A) + var data: [Double] = [] + var index = 0 + while true { + skipSpaces() + let value: Double? + let indexMod = index % argCount + if indexMod == 3 || indexMod == 4 { + value = readFlag() + } else { + value = readNum() + } + guard let doubleValue = value else { + return data + } + data.append(doubleValue) + index += 1 + } + return data + } + + private func skipSpaces() { + var currentCharacter = current + while let character = currentCharacter, Self.spaces.contains(character) { + currentCharacter = readNext() + } + } + + private func readFlag() -> Double? { + guard let ch = current else { + return .none + } + readNext() + switch ch { + case "0": + return 0 + case "1": + return 1 + default: + return .none + } + } + + fileprivate func readNum() -> Double? { + guard let ch = current else { + return .none + } + + guard ch >= "0" && ch <= "9" || ch == "." || ch == "-" else { + return .none + } + + var chars = [ch] + var hasDot = ch == "." + while let ch = readDigit(&hasDot) { + chars.append(ch) + } + + var buf = "" + buf.unicodeScalars.append(contentsOf: chars) + guard let value = Double(buf) else { + return .none + } + return value + } + + fileprivate func readDigit(_ hasDot: inout Bool) -> UnicodeScalar? { + if let ch = readNext() { + if (ch >= "0" && ch <= "9") || ch == "e" || (previous == "e" && ch == "-") { + return ch + } else if ch == "." && !hasDot { + hasDot = true + return ch + } + } + return nil + } + + fileprivate func isNum(ch: UnicodeScalar, hasDot: inout Bool) -> Bool { + switch ch { + case "0"..."9": + return true + case ".": + if hasDot { + return false + } + hasDot = true + default: + return true + } + return false + } + + @discardableResult + private func readNext() -> UnicodeScalar? { + previous = current + current = iterator.next() + return current + } + + private func isAcceptableSeparator(_ ch: UnicodeScalar?) -> Bool { + if let ch = ch { + return "\n\r\t ,".contains(String(ch)) + } + return false + } + + private func readSegmentType() -> PathSegment.SegmentType? { + while true { + if let type = getPathSegmentType() { + readNext() + return type + } + if readNext() == nil { + return nil + } + } + } + + fileprivate func getPathSegmentType() -> PathSegment.SegmentType? { + if let ch = current { + switch ch { + case "M": + return .M + case "m": + return .m + case "L": + return .L + case "l": + return .l + case "C": + return .C + case "c": + return .c + case "Q": + return .Q + case "q": + return .q + case "A": + return .A + case "a": + return .a + case "z", "Z": + return .z + case "H": + return .H + case "h": + return .h + case "V": + return .V + case "v": + return .v + case "S": + return .S + case "s": + return .s + case "T": + return .T + case "t": + return .t + default: + break + } + } + return nil + } + + fileprivate func getArgCount(segment: PathSegment.SegmentType) -> Int { + switch segment { + case .H, .h, .V, .v: + return 1 + case .M, .m, .L, .l, .T, .t: + return 2 + case .S, .s, .Q, .q: + return 4 + case .C, .c: + return 6 + case .A, .a: + return 7 + default: + return 0 + } + } +} + + +private let decodingMap: [String] = ["A", "A", "C", "A", "A", "A", "A", "H", "A", "A", "A", "L", "M", "A", "A", "A", "Q", "A", "S", "T", "A", "V", "A", "A", "A", "Z", "a", "a", "c", "a", "a", "a", "a", "h", "a", "a", "a", "l", "m", "a", "a", "a", "q", "a", "s", "t", "a", "v", "a", ".", "a", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", ","] +private func decodeStickerThumbnailData(_ data: Data) -> String { + var string = "M" + data.forEach { byte in + if byte >= 128 + 64 { + string.append(decodingMap[Int(byte) - 128 - 64]) + } else { + if byte >= 128 { + string.append(",") + } else if byte >= 64 { + string.append("-") + } + string.append("\(byte & 63)") + } + } + string.append("z") + return string +} + +public func generateStickerPlaceholderImage(data: Data?, size: CGSize, scale: CGFloat? = nil, imageSize: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor) -> UIImage? { + return generateImage(size, scale: scale, rotatedContext: { size, context in + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.setBlendMode(.copy) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.clear.cgColor) + } else { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foregroundColor.cgColor) + } + + if let data = data { + var path = decodeStickerThumbnailData(data) + if !path.hasSuffix("z") { + path = "\(path)z" + } + let reader = PathDataReader(input: path) + let segments = reader.read() + + let scale = max(size.width, size.height) / max(imageSize.width, imageSize.height) + context.scaleBy(x: scale, y: scale) + renderPath(segments, context: context) + } else { + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10.0, height: 10.0)) + UIGraphicsPushContext(context) + path.fill() + UIGraphicsPopContext() + } + }) +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/Contents.json new file mode 100644 index 0000000000..102a52576b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shadow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/shadow@3x.png b/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/shadow@3x.png new file mode 100644 index 0000000000..26ed3317e2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/shadow@3x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/Contents.json new file mode 100644 index 0000000000..31746b7078 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "smalleye.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/smalleye.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/smalleye.pdf new file mode 100644 index 0000000000..d1a8f74e11 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/smalleye.pdf @@ -0,0 +1,89 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.560059 0.300293 cm +1.000000 1.000000 1.000000 scn +6.439604 8.399902 m +3.031456 8.399902 0.941910 5.970886 0.174076 4.877690 c +-0.050043 4.558603 -0.057914 4.146943 0.151409 3.817960 c +0.892566 2.653118 2.962386 -0.000098 6.439604 -0.000098 c +9.916823 -0.000098 11.986644 2.653119 12.727800 3.817960 c +12.937123 4.146944 12.929253 4.558603 12.705133 4.877690 c +11.937300 5.970886 9.847753 8.399902 6.439604 8.399902 c +h +9.439605 4.199902 m +9.439605 2.543048 8.096458 1.199902 6.439604 1.199902 c +4.782750 1.199902 3.439604 2.543048 3.439604 4.199902 c +3.439604 5.856756 4.782750 7.199903 6.439604 7.199903 c +8.096458 7.199903 9.439605 5.856756 9.439605 4.199902 c +h +6.439604 2.399902 m +7.433717 2.399902 8.239604 3.205790 8.239604 4.199902 c +8.239604 5.194015 7.433717 5.999902 6.439604 5.999902 c +6.386777 5.999902 6.334482 5.997626 6.282810 5.993168 c +6.382505 5.818204 6.439462 5.615724 6.439462 5.399942 c +6.439462 4.737201 5.902204 4.199943 5.239462 4.199943 c +5.023717 4.199943 4.821270 4.256880 4.646326 4.356544 c +4.641876 4.304922 4.639604 4.252677 4.639604 4.199902 c +4.639604 3.205790 5.445492 2.399902 6.439604 2.399902 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1211 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 14.000000 9.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001301 00000 n +0000001324 00000 n +0000001496 00000 n +0000001570 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1629 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 1f9db5f2ae..b11833b811 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -105,7 +105,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { let chatListFilters = chatSelection.chatListFilters placeholder = placeholderValue - let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true) + let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false) chatListNode.passthroughPeerSelection = true chatListNode.disabledPeerSelected = { peer, _ in attemptDisabledItemSelection?(peer)