Various improvements

This commit is contained in:
Ali 2023-09-15 15:42:08 +02:00
parent 2028561610
commit 3b11fa400e
37 changed files with 1386 additions and 741 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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<ChatFolderUpdates?>()
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<Bool>(false, ignoreRepeated: true)
private let suggestedChatListNotice = Promise<ChatListNotice?>(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,22 +1755,31 @@ public final class ChatListNode: ListView {
displayArchiveIntro = .single(false)
}
let suggestedChatListNotice: Signal<ChatListNotice?, NoError>
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(
let twoStepData: Signal<TwoStepVerificationConfiguration?, NoError> = .single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map(Optional.init))
let suggestedChatListNoticeSignal: Signal<ChatListNotice?, NoError> = combineLatest(
getServerProvidedSuggestions(account: context.account),
context.engine.auth.twoStepVerificationConfiguration(),
twoStepData,
newSessionReviews(postbox: context.account.postbox)
)
|> mapToSignal { suggestions, configuration, newSessionReviews -> Signal<ChatListNotice?, NoError> in
if let newSessionReview = newSessionReviews.first {
return .single(.reviewLogin(newSessionReview: newSessionReview))
return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count))
}
if suggestions.contains(.setupPassword) {
if suggestions.contains(.setupPassword), let configuration {
var notSet = false
switch configuration {
case let .notSet(pendingEmail):
@ -1814,11 +1829,10 @@ public final class ChatListNode: ListView {
return .single(nil)
}
}
)
|> distinctUntilChanged
} else {
suggestedChatListNotice = .single(nil)
}
self.suggestedChatListNotice.set(suggestedChatListNoticeSignal)
})
let storageInfo: Signal<Double?, NoError>
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?) {

View File

@ -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 {

View File

@ -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)

View File

@ -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<PHAsset>, 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<PHAsset>, 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 {

View File

@ -34,6 +34,7 @@ swift_library(
"//submodules/TextFormat:TextFormat",
"//submodules/GZip:GZip",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
],
visibility = [
"//visibility:public",

View File

@ -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<UnicodeScalar> = 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..<end])))
isFirstSegment = false
index = end
}
return result
}
return nil
}
private func readData() -> [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
}
}
}

View File

@ -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)

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -46,6 +46,7 @@ swift_library(
"//submodules/GZip",
"//submodules/rlottie:RLottieBinding",
"//submodules/lottie-ios:Lottie",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
],
visibility = [
"//visibility:public",

View File

@ -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)

View File

@ -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

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AppBundle",
"//submodules/GZip",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
],
visibility = [
"//visibility:public",

View File

@ -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<Double> {
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<Double>) {
self.animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: cacheKey ?? "")
if let animationInstance = self.animationInstance {
@ -395,14 +427,19 @@ 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
}
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 {
self.updateImage()
}

View File

@ -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))
})
}
}

View File

@ -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<Double> {
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<Data?, NoError> { subscriber in
if attemptSynchronously {
if let path = mediaBox.completedResourcePath(file.resource), let contents = try? Data(contentsOf: URL(fileURLWithPath: path)) {
@ -56,7 +63,8 @@ public extension LottieComponent {
}
let dataDisposable = (mediaBox.resourceData(file.resource)
|> filter { data in return data.complete }).start(next: { data in
).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)
@ -64,6 +72,9 @@ public extension LottieComponent {
} else {
subscriber.putNext(nil)
}
} else {
subscriber.putNext(nil)
}
})
let fetchDisposable = mediaBox.fetchedResource(file.resource, parameters: nil).start()
@ -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))
})
}
}

View File

@ -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

View File

@ -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()
@ -160,6 +203,42 @@ private final class DurationLayer: CALayer {
return nullAction
}
func update(viewCount: Int32, isMin: Bool) {
if isMin {
self.contents = nil
} else {
let countString: String
if viewCount > 1000000 {
countString = "\(viewCount / 1000000)M"
} else if viewCount > 1000 {
countString = "\(viewCount / 1000)K"
} else {
countString = "\(viewCount)"
}
let string = NSAttributedString(string: countString, font: durationFont, textColor: .white)
let bounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let textSize = CGSize(width: ceil(bounds.width), height: ceil(bounds.height))
let sideInset: CGFloat = 6.0
let verticalInset: CGFloat = 2.0
let iconSpacing: CGFloat = -3.0
let image = generateImage(CGSize(width: viewCountImage.size.width + iconSpacing + textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.normal)
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor)
UIGraphicsPushContext(context)
viewCountImage.draw(in: CGRect(origin: CGPoint(x: 0.0, y: (size.height - viewCountImage.size.height) * 0.5), size: viewCountImage.size))
string.draw(in: bounds.offsetBy(dx: sideInset + viewCountImage.size.width + iconSpacing, dy: verticalInset))
UIGraphicsPopContext()
})
self.contents = image?.cgImage
}
}
func update(duration: Int32, isMin: Bool) {
if isMin {
self.contents = minDurationImage.cgImage
@ -172,13 +251,10 @@ private final class DurationLayer: CALayer {
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 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
}
@ -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?

View File

@ -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()
@ -318,13 +330,10 @@ private final class DurationLayer: CALayer {
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)
}
}
}

View File

@ -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()

View File

@ -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:

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
)),

View File

@ -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()
}
}
}
}

View File

@ -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",
],
)

View File

@ -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<UnicodeScalar> = 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..<end])))
isFirstSegment = false
index = end
}
return result
}
return nil
}
private func readData() -> [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()
}
})
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

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

View File

@ -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

View File

@ -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)