From 3b11fa400e72760130addfa24fe9c71a846805d2 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 15 Sep 2023 15:42:08 +0200 Subject: [PATCH] Various improvements --- .../Sources/ChatListContainerItemNode.swift | 4 +- .../Sources/ChatListControllerNode.swift | 7 +- .../Sources/ChatListSearchListPaneNode.swift | 4 + .../Sources/Node/ChatListNode.swift | 157 ++--- .../Sources/Node/ChatListNodeEntries.swift | 2 +- .../Node/ChatListStorageInfoItem.swift | 8 +- .../Sources/MediaPickerGridItem.swift | 101 +++- submodules/ReactionSelectionNode/BUILD | 1 + .../Sources/ReactionSelectionNode.swift | 1 + submodules/ShimmerEffect/BUILD | 1 + .../Sources/StickerShimmerEffectNode.swift | 561 +---------------- .../Sources/ChatEntityKeyboardInputNode.swift | 2 +- .../Components/EmojiTextAttachmentView/BUILD | 1 + .../Sources/EmojiTextAttachmentView.swift | 1 + .../Components/EntityKeyboard/BUILD | 1 + .../Sources/EmojiPagerContentComponent.swift | 1 + .../Sources/EmojiSearchStatusComponent.swift | 4 +- .../Components/LottieComponent/BUILD | 1 + .../Sources/LottieComponent.swift | 49 +- .../Sources/LottieComponentEmojiContent.swift | 4 +- .../LottieComponentResourceContent.swift | 30 +- .../MediaEditor/Sources/MediaEditor.swift | 12 +- .../Sources/PeerInfoStoryPaneNode.swift | 311 +++++++++- .../Sources/PeerInfoVisualMediaPaneNode.swift | 85 ++- .../Sources/PeerSelectionControllerNode.swift | 2 +- .../StoryContentCaptionComponent.swift | 10 + .../Sources/StoryItemContentComponent.swift | 2 +- .../Sources/StoryItemLoadingEffectView.swift | 39 +- .../Sources/StoryItemOverlaysView.swift | 4 +- .../Sources/TextFieldComponent.swift | 15 +- .../GenerateStickerPlaceholderImage/BUILD | 18 + .../GenerateStickerPlaceholderImage.swift | 564 ++++++++++++++++++ .../MediaGridShadow.imageset/Contents.json | 21 + .../MediaGridShadow.imageset/shadow@3x.png | Bin 0 -> 27629 bytes .../MediaGridViewCount.imageset/Contents.json | 12 + .../MediaGridViewCount.imageset/smalleye.pdf | 89 +++ .../ContactMultiselectionControllerNode.swift | 2 +- 37 files changed, 1386 insertions(+), 741 deletions(-) create mode 100644 submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage/BUILD create mode 100644 submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage/Sources/GenerateStickerPlaceholderImage.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/shadow@3x.png create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/smalleye.pdf 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 0000000000000000000000000000000000000000..26ed3317e2aab073478f997761c49bd6d46db65a GIT binary patch literal 27629 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

$xQAZ(7ZCtHU-depvpM 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 literal 0 HcmV?d00001 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)