Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AccountContext",
module_name = "AccountContext",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/TelegramAudio:TelegramAudio",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/TemporaryCachedPeerDataManager:TemporaryCachedPeerDataManager",
"//submodules/DeviceLocationManager:DeviceLocationManager",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TextFormat:TextFormat",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramCore/FlatBuffers",
"//submodules/TelegramCore/FlatSerialization",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,19 @@
//
// AccountContext.h
// AccountContext
//
// Created by Peter on 8/1/19.
// Copyright © 2019 Telegram Messenger LLP. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for AccountContext.
FOUNDATION_EXPORT double AccountContextVersionNumber;
//! Project version string for AccountContext.
FOUNDATION_EXPORT const unsigned char AccountContextVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <AccountContext/PublicHeader.h>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
import Foundation
import UIKit
public struct AttachmentMainButtonState {
public enum Background {
case color(UIColor)
case premium
public var colorValue: UIColor? {
if case let .color(color) = self {
return color
}
return nil
}
}
public enum Progress: Equatable {
case none
case side
case center
}
public enum Font: Equatable {
case regular
case bold
}
public enum Position: String, Equatable {
case top
case bottom
case left
case right
}
public let text: String?
public let badge: String?
public let font: Font
public let background: Background
public let textColor: UIColor
public let isVisible: Bool
public let progress: Progress
public let isEnabled: Bool
public let hasShimmer: Bool
public let iconName: String?
public let smallSpacing: Bool
public let position: Position?
public init(
text: String?,
badge: String? = nil,
font: Font,
background: Background,
textColor: UIColor,
isVisible: Bool,
progress: Progress,
isEnabled: Bool,
hasShimmer: Bool,
iconName: String? = nil,
smallSpacing: Bool = false,
position: Position? = nil
) {
self.text = text
self.badge = badge
self.font = font
self.background = background
self.textColor = textColor
self.isVisible = isVisible
self.progress = progress
self.isEnabled = isEnabled
self.hasShimmer = hasShimmer
self.iconName = iconName
self.smallSpacing = smallSpacing
self.position = position
}
public static var initial: AttachmentMainButtonState {
return AttachmentMainButtonState(text: nil, font: .bold, background: .color(.clear), textColor: .clear, isVisible: false, progress: .none, isEnabled: false, hasShimmer: false)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
import Foundation
import Postbox
import Display
public enum ChatHistoryInitialSearchLocation: Equatable {
case index(MessageIndex)
case id(MessageId)
}
public struct MessageHistoryScrollToSubject: Equatable {
public struct Quote: Equatable {
public var string: String
public var offset: Int?
public init(string: String, offset: Int?) {
self.string = string
self.offset = offset
}
}
public var index: MessageHistoryAnchorIndex
public var quote: Quote?
public var todoTaskId: Int32?
public var setupReply: Bool
public init(index: MessageHistoryAnchorIndex, quote: Quote? = nil, todoTaskId: Int32? = nil, setupReply: Bool = false) {
self.index = index
self.quote = quote
self.todoTaskId = todoTaskId
self.setupReply = setupReply
}
}
public struct MessageHistoryInitialSearchSubject: Equatable {
public struct Quote: Equatable {
public var string: String
public var offset: Int?
public init(string: String, offset: Int?) {
self.string = string
self.offset = offset
}
}
public var location: ChatHistoryInitialSearchLocation
public var quote: Quote?
public var todoTaskId: Int32?
public init(location: ChatHistoryInitialSearchLocation, quote: Quote? = nil, todoTaskId: Int32? = nil) {
self.location = location
self.quote = quote
self.todoTaskId = todoTaskId
}
}
public enum ChatHistoryLocation: Equatable {
case Initial(count: Int)
case InitialSearch(subject: MessageHistoryInitialSearchSubject, count: Int, highlight: Bool, setupReply: Bool)
case Navigation(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, highlight: Bool)
case Scroll(subject: MessageHistoryScrollToSubject, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool, setupReply: Bool)
}
public struct ChatHistoryLocationInput: Equatable {
public var content: ChatHistoryLocation
public var id: Int32
public init(content: ChatHistoryLocation, id: Int32) {
self.content = content
self.id = id
}
}
@@ -0,0 +1,31 @@
import Foundation
import UIKit
import Display
import TelegramCore
public enum ChatListControllerLocation: Equatable {
case chatList(groupId: EngineChatList.Group)
case forum(peerId: EnginePeer.Id)
case savedMessagesChats(peerId: EnginePeer.Id)
}
public protocol ChatListController: ViewController {
var context: AccountContext { get }
var location: ChatListControllerLocation { get }
var lockViewFrame: CGRect? { get }
var isSearchActive: Bool { get }
func activateSearch(filter: ChatListSearchFilter, query: String?)
func deactivateSearch(animated: Bool)
func activateCompose()
func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void)
func playSignUpCompletedAnimation()
func navigateToFolder(folderId: Int32, completion: @escaping () -> Void)
func openStories(peerId: EnginePeer.Id)
func openStoriesFromNotification(peerId: EnginePeer.Id, storyId: Int32)
func resetForumStackIfOpen()
}
@@ -0,0 +1,161 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
public struct ChatListNodeAdditionalCategory {
public enum Appearance: Equatable {
case option(sectionTitle: String?)
case action
}
public var id: Int
public var icon: UIImage?
public var smallIcon: UIImage?
public var title: String
public var appearance: Appearance
public init(id: Int, icon: UIImage?, smallIcon: UIImage?, title: String, appearance: Appearance = .option(sectionTitle: nil)) {
self.id = id
self.icon = icon
self.smallIcon = smallIcon
self.title = title
self.appearance = appearance
}
}
public struct ContactMultiselectionControllerAdditionalCategories {
public var categories: [ChatListNodeAdditionalCategory]
public var selectedCategories: Set<Int>
public init(categories: [ChatListNodeAdditionalCategory], selectedCategories: Set<Int>) {
self.categories = categories
self.selectedCategories = selectedCategories
}
}
public enum ContactMultiselectionControllerMode {
public struct ChatSelection {
public var title: String
public var searchPlaceholder: String
public var selectedChats: Set<EnginePeer.Id>
public var additionalCategories: ContactMultiselectionControllerAdditionalCategories?
public var chatListFilters: [ChatListFilter]?
public var displayAutoremoveTimeout: Bool
public var displayPresence: Bool
public var onlyUsers: Bool
public var disableChannels: Bool
public var disableBots: Bool
public var disableContacts: Bool
public init(
title: String,
searchPlaceholder: String,
selectedChats: Set<EnginePeer.Id>,
additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
chatListFilters: [ChatListFilter]?,
displayAutoremoveTimeout: Bool = false,
displayPresence: Bool = false,
onlyUsers: Bool = false,
disableChannels: Bool = false,
disableBots: Bool = false,
disableContacts: Bool = false
) {
self.title = title
self.searchPlaceholder = searchPlaceholder
self.selectedChats = selectedChats
self.additionalCategories = additionalCategories
self.chatListFilters = chatListFilters
self.displayAutoremoveTimeout = displayAutoremoveTimeout
self.displayPresence = displayPresence
self.onlyUsers = onlyUsers
self.disableChannels = disableChannels
self.disableBots = disableBots
self.disableContacts = disableContacts
}
}
case groupCreation(isCall: Bool)
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
case channelCreation
case chatSelection(ChatSelection)
case premiumGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, selectToday: Bool, hasActions: Bool)
case requestedUsersSelection(isBot: Bool?, isPremium: Bool?)
}
public enum ContactListFilter {
case excludeWithoutPhoneNumbers
case excludeSelf
case excludeBots
case exclude([EnginePeer.Id])
case disable([EnginePeer.Id])
}
public final class ContactMultiselectionControllerParams {
public let context: AccountContext
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
public let title: String?
public let mode: ContactMultiselectionControllerMode
public let options: Signal<[ContactListAdditionalOption], NoError>
public let filters: [ContactListFilter]
public let onlyWriteable: Bool
public let isGroupInvitation: Bool
public let isPeerEnabled: ((EnginePeer) -> Bool)?
public let attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?
public let alwaysEnabled: Bool
public let limit: Int32?
public let reachedLimit: ((Int32) -> Void)?
public let openProfile: ((EnginePeer) -> Void)?
public let sendMessage: ((EnginePeer) -> Void)?
public let initialSelectedPeers: [EnginePeer]
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
title: String? = nil,
mode: ContactMultiselectionControllerMode,
options: Signal<[ContactListAdditionalOption], NoError> = .single([]),
filters: [ContactListFilter] = [.excludeSelf],
onlyWriteable: Bool = false,
isGroupInvitation: Bool = false,
isPeerEnabled: ((EnginePeer) -> Bool)? = nil,
attemptDisabledItemSelection: ((EnginePeer, ChatListDisabledPeerReason) -> Void)? = nil,
alwaysEnabled: Bool = false,
limit: Int32? = nil,
reachedLimit: ((Int32) -> Void)? = nil,
openProfile: ((EnginePeer) -> Void)? = nil,
sendMessage: ((EnginePeer) -> Void)? = nil,
initialSelectedPeers: [EnginePeer] = []
) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.title = title
self.mode = mode
self.options = options
self.filters = filters
self.onlyWriteable = onlyWriteable
self.isGroupInvitation = isGroupInvitation
self.isPeerEnabled = isPeerEnabled
self.attemptDisabledItemSelection = attemptDisabledItemSelection
self.alwaysEnabled = alwaysEnabled
self.limit = limit
self.reachedLimit = reachedLimit
self.openProfile = openProfile
self.sendMessage = sendMessage
self.initialSelectedPeers = initialSelectedPeers
}
}
public enum ContactMultiselectionResult {
case none
case result(peerIds: [ContactListPeerId], additionalOptionIds: [Int])
}
public protocol ContactMultiselectionController: ViewController {
var result: Signal<ContactMultiselectionResult, NoError> { get }
var displayProgress: Bool { get set }
var dismissed: (() -> Void)? { get set }
var isCallVideoOptionSelected: Bool { get }
}
@@ -0,0 +1,165 @@
import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
public protocol ContactSelectionController: ViewController {
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { get }
var displayProgress: Bool { get set }
var dismissed: (() -> Void)? { get set }
var presentScheduleTimePicker: (@escaping (Int32, Int32?) -> Void) -> Void { get set }
func dismissSearch()
}
public enum ContactSelectionControllerMode {
case generic
case starsGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, hasActions: Bool, showSelf: Bool, selfSubtitle: String?)
}
public struct ContactListAdditionalOption: Equatable {
public enum Style: Equatable {
case accent
case generic
}
public let title: String
public let subtitle: String?
public let icon: ContactListActionItemIcon
public let style: Style
public let action: () -> Void
public let clearHighlightAutomatically: Bool
public init(title: String, subtitle: String? = nil, icon: ContactListActionItemIcon, style: Style = .accent, action: @escaping () -> Void, clearHighlightAutomatically: Bool = false) {
self.title = title
self.subtitle = subtitle
self.icon = icon
self.style = style
self.action = action
self.clearHighlightAutomatically = clearHighlightAutomatically
}
public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool {
return lhs.title == rhs.title && lhs.subtitle == rhs.subtitle && lhs.icon == rhs.icon && lhs.style == rhs.style
}
}
public enum ContactListPeerId: Hashable {
case peer(PeerId)
case deviceContact(DeviceContactStableId)
}
public enum ContactListAction: Equatable {
case generic
case voiceCall
case videoCall
case more
}
public enum ContactListPeer: Equatable {
case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
public var id: ContactListPeerId {
switch self {
case let .peer(peer, _, _):
return .peer(peer.id)
case let .deviceContact(id, _):
return .deviceContact(id)
}
}
public var indexName: PeerIndexNameRepresentation {
switch self {
case let .peer(peer, _, _):
return peer.indexName
case let .deviceContact(_, contact):
return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "")
}
}
public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
switch lhs {
case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount):
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
return true
} else {
return false
}
case let .deviceContact(id, contact):
if case .deviceContact(id, contact) = rhs {
return true
} else {
return false
}
}
}
}
public final class ContactSelectionControllerParams {
public enum MultipleSelectionMode {
case disabled
case possible
case always
}
public enum Style {
case glass
case legacy
}
public let context: AccountContext
public let style: Style
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
public let mode: ContactSelectionControllerMode
public let autoDismiss: Bool
public let title: (PresentationStrings) -> String
public let options: Signal<[ContactListAdditionalOption], NoError>
public let displayDeviceContacts: Bool
public let displayCallIcons: Bool
public let multipleSelection: MultipleSelectionMode
public let requirePhoneNumbers: Bool
public let allowChannelsInSearch: Bool
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
public let isPeerEnabled: (ContactListPeer) -> Bool
public let openProfile: ((EnginePeer) -> Void)?
public let sendMessage: ((EnginePeer) -> Void)?
public init(
context: AccountContext,
style: Style = .legacy,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
mode: ContactSelectionControllerMode = .generic,
autoDismiss: Bool = true,
title: @escaping (PresentationStrings) -> String,
options: Signal<[ContactListAdditionalOption], NoError> = .single([]),
displayDeviceContacts: Bool = false,
displayCallIcons: Bool = false,
multipleSelection: MultipleSelectionMode = .disabled,
requirePhoneNumbers: Bool = false,
allowChannelsInSearch: Bool = false,
confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) },
isPeerEnabled: @escaping (ContactListPeer) -> Bool = { _ in true },
openProfile: ((EnginePeer) -> Void)? = nil,
sendMessage: ((EnginePeer) -> Void)? = nil
) {
self.context = context
self.style = style
self.updatedPresentationData = updatedPresentationData
self.mode = mode
self.autoDismiss = autoDismiss
self.title = title
self.options = options
self.displayDeviceContacts = displayDeviceContacts
self.displayCallIcons = displayCallIcons
self.multipleSelection = multipleSelection
self.requirePhoneNumbers = requirePhoneNumbers
self.allowChannelsInSearch = allowChannelsInSearch
self.confirmation = confirmation
self.isPeerEnabled = isPeerEnabled
self.openProfile = openProfile
self.sendMessage = sendMessage
}
}
@@ -0,0 +1,26 @@
import Foundation
import TelegramCore
public class CountriesConfiguration {
public let countries: [Country]
public let countriesByPrefix: [String: (Country, Country.CountryCode)]
public init(countries: [Country]) {
self.countries = countries
var countriesByPrefix: [String: (Country, Country.CountryCode)] = [:]
for country in countries {
for code in country.countryCodes {
if !code.prefixes.isEmpty {
for prefix in code.prefixes {
countriesByPrefix["\(code.code)\(prefix)"] = (country, code)
}
} else {
countriesByPrefix[code.code] = (country, code)
}
}
}
self.countriesByPrefix = countriesByPrefix
}
}
@@ -0,0 +1,681 @@
import Foundation
import Contacts
import TelegramCore
import FlatBuffers
import FlatSerialization
public final class DeviceContactPhoneNumberData: Equatable {
public let label: String
public let value: String
public init(label: String, value: String) {
self.label = label
self.value = value
}
init(flatBuffersObject: TelegramCore_DeviceContactPhoneNumberData) {
self.label = flatBuffersObject.label
self.value = flatBuffersObject.value
}
public func encodeToFlatBuffers(builder: inout FlatBufferBuilder) -> Offset {
let labelOffset = builder.create(string: self.label)
let valueOffset = builder.create(string: self.value)
return TelegramCore_DeviceContactPhoneNumberData.createDeviceContactPhoneNumberData(
&builder,
labelOffset: labelOffset,
valueOffset: valueOffset
)
}
public static func == (lhs: DeviceContactPhoneNumberData, rhs: DeviceContactPhoneNumberData) -> Bool {
if lhs.label != rhs.label {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
}
public final class DeviceContactEmailAddressData: Equatable {
public let label: String
public let value: String
public init(label: String, value: String) {
self.label = label
self.value = value
}
public static func == (lhs: DeviceContactEmailAddressData, rhs: DeviceContactEmailAddressData) -> Bool {
if lhs.label != rhs.label {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
}
public final class DeviceContactUrlData: Equatable {
public let label: String
public let value: String
public init(label: String, value: String) {
self.label = label
self.value = value
}
public static func == (lhs: DeviceContactUrlData, rhs: DeviceContactUrlData) -> Bool {
if lhs.label != rhs.label {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
}
public final class DeviceContactAddressData: Equatable, Hashable {
public let label: String
public let street1: String
public let street2: String
public let state: String
public let city: String
public let country: String
public let postcode: String
public init(label: String, street1: String, street2: String, state: String, city: String, country: String, postcode: String) {
self.label = label
self.street1 = street1
self.street2 = street2
self.state = state
self.city = city
self.country = country
self.postcode = postcode
}
public static func == (lhs: DeviceContactAddressData, rhs: DeviceContactAddressData) -> Bool {
if lhs.label != rhs.label {
return false
}
if lhs.street1 != rhs.street1 {
return false
}
if lhs.street2 != rhs.street2 {
return false
}
if lhs.state != rhs.state {
return false
}
if lhs.city != rhs.city {
return false
}
if lhs.country != rhs.country {
return false
}
if lhs.postcode != rhs.postcode {
return false
}
return true
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.label)
hasher.combine(self.street1)
hasher.combine(self.street2)
hasher.combine(self.state)
hasher.combine(self.city)
hasher.combine(self.country)
hasher.combine(self.postcode)
}
}
public final class DeviceContactSocialProfileData: Equatable, Hashable {
public let label: String
public let service: String
public let username: String
public let url: String
public init(label: String, service: String, username: String, url: String) {
self.label = label
self.service = service
self.username = username
self.url = url
}
public static func == (lhs: DeviceContactSocialProfileData, rhs: DeviceContactSocialProfileData) -> Bool {
if lhs.label != rhs.label {
return false
}
if lhs.service != rhs.service {
return false
}
if lhs.username != rhs.username {
return false
}
if lhs.url != rhs.url {
return false
}
return true
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.label)
hasher.combine(self.service)
hasher.combine(self.username)
hasher.combine(self.url)
}
}
public final class DeviceContactInstantMessagingProfileData: Equatable, Hashable {
public let label: String
public let service: String
public let username: String
public init(label: String, service: String, username: String) {
self.label = label
self.service = service
self.username = username
}
public static func == (lhs: DeviceContactInstantMessagingProfileData, rhs: DeviceContactInstantMessagingProfileData) -> Bool {
if lhs.label != rhs.label {
return false
}
if lhs.service != rhs.service {
return false
}
if lhs.username != rhs.username {
return false
}
return true
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.label)
hasher.combine(self.service)
hasher.combine(self.username)
}
}
public let phonebookUsernamePathPrefix = "@id"
private let phonebookUsernamePrefix = "https://t.me/" + phonebookUsernamePathPrefix
public extension DeviceContactUrlData {
convenience init(appProfile: EnginePeer.Id) {
self.init(label: "Telegram", value: "\(phonebookUsernamePrefix)\(appProfile.id._internalGetInt64Value())")
}
}
public func parseAppSpecificContactReference(_ value: String) -> EnginePeer.Id? {
if !value.hasPrefix(phonebookUsernamePrefix) {
return nil
}
let idString = String(value[value.index(value.startIndex, offsetBy: phonebookUsernamePrefix.count)...])
if let id = Int64(idString) {
return EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id))
}
return nil
}
public final class DeviceContactBasicData: Equatable {
public let firstName: String
public let lastName: String
public let phoneNumbers: [DeviceContactPhoneNumberData]
public init(firstName: String, lastName: String, phoneNumbers: [DeviceContactPhoneNumberData]) {
self.firstName = firstName
self.lastName = lastName
self.phoneNumbers = phoneNumbers
}
public init(flatBuffersObject: TelegramCore_StoredDeviceContactData) {
self.firstName = flatBuffersObject.firstName
self.lastName = flatBuffersObject.lastName
if flatBuffersObject.phoneNumbersCount == 1 {
self.phoneNumbers = [
DeviceContactPhoneNumberData(flatBuffersObject: flatBuffersObject.phoneNumbers(at: 0)!)
]
} else {
var phoneNumbers: [DeviceContactPhoneNumberData] = []
for i in 0 ..< flatBuffersObject.phoneNumbersCount {
phoneNumbers.append(DeviceContactPhoneNumberData(flatBuffersObject: flatBuffersObject.phoneNumbers(at: i)!))
}
self.phoneNumbers = phoneNumbers
}
}
public func encodeToFlatBuffers(builder: inout FlatBufferBuilder) -> Offset {
let phoneNumberOffsets = self.phoneNumbers.map { $0.encodeToFlatBuffers(builder: &builder) }
let phoneNumberOffset = builder.createVector(ofOffsets: phoneNumberOffsets, len: phoneNumberOffsets.count)
let firstNameOffset = builder.create(string: self.firstName)
let lastNameOffset = builder.create(string: self.lastName)
return TelegramCore_StoredDeviceContactData.createStoredDeviceContactData(
&builder,
firstNameOffset: firstNameOffset,
lastNameOffset: lastNameOffset,
phoneNumbersVectorOffset: phoneNumberOffset
)
}
public static func ==(lhs: DeviceContactBasicData, rhs: DeviceContactBasicData) -> Bool {
if lhs.firstName != rhs.firstName {
return false
}
if lhs.lastName != rhs.lastName {
return false
}
if lhs.phoneNumbers != rhs.phoneNumbers {
return false
}
return true
}
}
public final class DeviceContactDataState: Codable {
private enum CodingKeys: String, CodingKey {
case contactsData
case contactsKeys
case telegramReferencesKeys
case telegramReferencesValues
case stateToken
}
public let contacts: [String: DeviceContactBasicData]
public let telegramReferences: [EnginePeer.Id: String]
public let stateToken: Data?
public init(contacts: [String: DeviceContactBasicData], telegramReferences: [EnginePeer.Id: String], stateToken: Data?) {
self.contacts = contacts
self.telegramReferences = telegramReferences
self.stateToken = stateToken
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let contactsData = try container.decode([Data].self, forKey: .contactsData).map { data in
var byteBuffer = ByteBuffer(data: data)
let deserializedValue = FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_StoredDeviceContactData
let parsedValue = DeviceContactBasicData(flatBuffersObject: deserializedValue)
return parsedValue
}
let contactsKeys = try container.decode([String].self, forKey: .contactsKeys)
var contacts: [String: DeviceContactBasicData] = [:]
for i in 0 ..< min(contactsData.count, contactsKeys.count) {
contacts[contactsKeys[i]] = contactsData[i]
}
self.contacts = contacts
let telegramReferencesKeys = try container.decode([Int64].self, forKey: .telegramReferencesKeys).map { value in
return EnginePeer.Id(value)
}
let telegramReferencesValues = try container.decode([String].self, forKey: .telegramReferencesValues)
var telegramReferences: [EnginePeer.Id: String] = [:]
for i in 0 ..< min(telegramReferencesValues.count, telegramReferencesKeys.count) {
telegramReferences[telegramReferencesKeys[i]] = telegramReferencesValues[i]
}
self.telegramReferences = telegramReferences
self.stateToken = try container.decodeIfPresent(Data.self, forKey: .stateToken)
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var contactsData: [Data] = []
var contactsKeys: [String] = []
for (key, contact) in self.contacts {
var builder = FlatBufferBuilder(initialSize: 1024)
let offset = contact.encodeToFlatBuffers(builder: &builder)
builder.finish(offset: offset)
contactsData.append(builder.data)
contactsKeys.append(key)
}
var telegramReferencesKeys: [Int64] = []
var telegramReferencesValues: [String] = []
for (key, value) in self.telegramReferences {
telegramReferencesKeys.append(key.toInt64())
telegramReferencesValues.append(value)
}
try container.encode(contactsKeys, forKey: .contactsKeys)
try container.encode(contactsData, forKey: .contactsData)
try container.encode(telegramReferencesKeys, forKey: .telegramReferencesKeys)
try container.encode(telegramReferencesValues, forKey: .telegramReferencesValues)
try container.encodeIfPresent(stateToken, forKey: .stateToken)
}
}
public final class DeviceContactBasicDataWithReference: Equatable {
public let stableId: DeviceContactStableId
public let basicData: DeviceContactBasicData
public init(stableId: DeviceContactStableId, basicData: DeviceContactBasicData) {
self.stableId = stableId
self.basicData = basicData
}
public static func ==(lhs: DeviceContactBasicDataWithReference, rhs: DeviceContactBasicDataWithReference) -> Bool {
return lhs.stableId == rhs.stableId && lhs.basicData == rhs.basicData
}
}
public final class DeviceContactExtendedData: Equatable {
public let basicData: DeviceContactBasicData
public let middleName: String
public let prefix: String
public let suffix: String
public let organization: String
public let jobTitle: String
public let department: String
public let emailAddresses: [DeviceContactEmailAddressData]
public let urls: [DeviceContactUrlData]
public let addresses: [DeviceContactAddressData]
public let birthdayDate: Date?
public let socialProfiles: [DeviceContactSocialProfileData]
public let instantMessagingProfiles: [DeviceContactInstantMessagingProfileData]
public let note: String
public init(basicData: DeviceContactBasicData, middleName: String, prefix: String, suffix: String, organization: String, jobTitle: String, department: String, emailAddresses: [DeviceContactEmailAddressData], urls: [DeviceContactUrlData], addresses: [DeviceContactAddressData], birthdayDate: Date?, socialProfiles: [DeviceContactSocialProfileData], instantMessagingProfiles: [DeviceContactInstantMessagingProfileData], note: String) {
self.basicData = basicData
self.middleName = middleName
self.prefix = prefix
self.suffix = suffix
self.organization = organization
self.jobTitle = jobTitle
self.department = department
self.emailAddresses = emailAddresses
self.urls = urls
self.addresses = addresses
self.birthdayDate = birthdayDate
self.socialProfiles = socialProfiles
self.instantMessagingProfiles = instantMessagingProfiles
self.note = note
}
public static func ==(lhs: DeviceContactExtendedData, rhs: DeviceContactExtendedData) -> Bool {
if lhs.basicData != rhs.basicData {
return false
}
if lhs.middleName != rhs.middleName {
return false
}
if lhs.prefix != rhs.prefix {
return false
}
if lhs.suffix != rhs.suffix {
return false
}
if lhs.organization != rhs.organization {
return false
}
if lhs.jobTitle != rhs.jobTitle {
return false
}
if lhs.department != rhs.department {
return false
}
if lhs.emailAddresses != rhs.emailAddresses {
return false
}
if lhs.urls != rhs.urls {
return false
}
if lhs.addresses != rhs.addresses {
return false
}
if lhs.birthdayDate != rhs.birthdayDate {
return false
}
if lhs.socialProfiles != rhs.socialProfiles {
return false
}
if lhs.instantMessagingProfiles != rhs.instantMessagingProfiles {
return false
}
if lhs.note != rhs.note {
return false
}
return true
}
}
public extension DeviceContactExtendedData {
convenience init?(vcard: Data) {
guard let contact = (try? CNContactVCardSerialization.contacts(with: vcard))?.first else {
return nil
}
self.init(contact: contact)
}
func asMutableCNContact() -> CNMutableContact {
let contact = CNMutableContact()
contact.givenName = self.basicData.firstName
contact.familyName = self.basicData.lastName
contact.namePrefix = self.prefix
contact.nameSuffix = self.suffix
contact.middleName = self.middleName
contact.phoneNumbers = self.basicData.phoneNumbers.map { phoneNumber -> CNLabeledValue<CNPhoneNumber> in
return CNLabeledValue<CNPhoneNumber>(label: phoneNumber.label, value: CNPhoneNumber(stringValue: phoneNumber.value))
}
contact.emailAddresses = self.emailAddresses.map { email -> CNLabeledValue<NSString> in
CNLabeledValue<NSString>(label: email.label, value: email.value as NSString)
}
contact.urlAddresses = self.urls.map { url -> CNLabeledValue<NSString> in
CNLabeledValue<NSString>(label: url.label, value: url.value as NSString)
}
contact.socialProfiles = self.socialProfiles.map({ profile -> CNLabeledValue<CNSocialProfile> in
return CNLabeledValue<CNSocialProfile>(label: profile.label, value: CNSocialProfile(urlString: nil, username: profile.username, userIdentifier: nil, service: profile.service))
})
contact.instantMessageAddresses = self.instantMessagingProfiles.map({ profile -> CNLabeledValue<CNInstantMessageAddress> in
return CNLabeledValue<CNInstantMessageAddress>(label: profile.label, value: CNInstantMessageAddress(username: profile.username, service: profile.service))
})
contact.postalAddresses = self.addresses.map({ address -> CNLabeledValue<CNPostalAddress> in
let value = CNMutablePostalAddress()
value.street = address.street1 + "\n" + address.street2
value.state = address.state
value.city = address.city
value.country = address.country
value.postalCode = address.postcode
return CNLabeledValue<CNPostalAddress>(label: address.label, value: value)
})
if let birthdayDate = self.birthdayDate {
contact.birthday = Calendar(identifier: .gregorian).dateComponents([.day, .month, .year], from: birthdayDate)
}
return contact
}
func serializedVCard() -> String? {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
guard let data = try? CNContactVCardSerialization.data(with: [self.asMutableCNContact()]) else {
return nil
}
return String(data: data, encoding: .utf8)
}
return nil
}
convenience init(contact: CNContact) {
var phoneNumbers: [DeviceContactPhoneNumberData] = []
for number in contact.phoneNumbers {
phoneNumbers.append(DeviceContactPhoneNumberData(label: number.label ?? "", value: number.value.stringValue))
}
var emailAddresses: [DeviceContactEmailAddressData] = []
for email in contact.emailAddresses {
emailAddresses.append(DeviceContactEmailAddressData(label: email.label ?? "", value: email.value as String))
}
var urls: [DeviceContactUrlData] = []
for url in contact.urlAddresses {
urls.append(DeviceContactUrlData(label: url.label ?? "", value: url.value as String))
}
var addresses: [DeviceContactAddressData] = []
for address in contact.postalAddresses {
addresses.append(DeviceContactAddressData(label: address.label ?? "", street1: address.value.street, street2: "", state: address.value.state, city: address.value.city, country: address.value.country, postcode: address.value.postalCode))
}
var birthdayDate: Date?
if let birthday = contact.birthday {
if let date = birthday.date {
birthdayDate = date
}
}
var socialProfiles: [DeviceContactSocialProfileData] = []
for profile in contact.socialProfiles {
socialProfiles.append(DeviceContactSocialProfileData(label: profile.label ?? "", service: profile.value.service, username: profile.value.username, url: profile.value.urlString))
}
var instantMessagingProfiles: [DeviceContactInstantMessagingProfileData] = []
for profile in contact.instantMessageAddresses {
instantMessagingProfiles.append(DeviceContactInstantMessagingProfileData(label: profile.label ?? "", service: profile.value.service, username: profile.value.username))
}
let basicData = DeviceContactBasicData(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers)
self.init(basicData: basicData, middleName: contact.middleName, prefix: contact.namePrefix, suffix: contact.nameSuffix, organization: contact.organizationName, jobTitle: contact.jobTitle, department: contact.departmentName, emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: birthdayDate, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles, note: "")
}
var isPrimitive: Bool {
if self.basicData.phoneNumbers.count > 1 {
return false
}
if !self.organization.isEmpty {
return false
}
if !self.jobTitle.isEmpty {
return false
}
if !self.department.isEmpty {
return false
}
if !self.emailAddresses.isEmpty {
return false
}
if !self.urls.isEmpty {
return false
}
if !self.addresses.isEmpty {
return false
}
if self.birthdayDate != nil {
return false
}
if !self.socialProfiles.isEmpty {
return false
}
if !self.instantMessagingProfiles.isEmpty {
return false
}
if !self.note.isEmpty {
return false
}
return true
}
}
public extension DeviceContactExtendedData {
convenience init?(peer: EnginePeer) {
guard case let .user(user) = peer else {
return nil
}
var phoneNumbers: [DeviceContactPhoneNumberData] = []
if let phone = user.phone, !phone.isEmpty {
phoneNumbers.append(DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: phone))
}
self.init(basicData: DeviceContactBasicData(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumbers: phoneNumbers), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
}
}
public extension DeviceContactAddressData {
var asPostalAddress: CNPostalAddress {
let address = CNMutablePostalAddress()
if !self.street1.isEmpty {
address.street = self.street1
}
if !self.city.isEmpty {
address.city = self.city
}
if !self.state.isEmpty {
address.state = self.state
}
if !self.country.isEmpty {
address.country = self.country
}
if !self.postcode.isEmpty {
address.postalCode = self.postcode
}
return address
}
var dictionary: [String: String] {
var dictionary: [String: String] = [:]
if !self.street1.isEmpty {
dictionary["Street"] = self.street1
}
if !self.city.isEmpty {
dictionary["City"] = self.city
}
if !self.state.isEmpty {
dictionary["State"] = self.state
}
if !self.country.isEmpty {
dictionary["Country"] = self.country
}
if !self.postcode.isEmpty {
dictionary["ZIP"] = self.postcode
}
return dictionary
}
var string: String {
var array: [String] = []
if !self.street1.isEmpty {
array.append(self.street1)
}
if !self.city.isEmpty {
array.append(self.city)
}
if !self.state.isEmpty {
array.append(self.state)
}
if !self.country.isEmpty {
array.append(self.country)
}
if !self.postcode.isEmpty {
array.append(self.postcode)
}
return array.joined(separator: " ")
}
var displayString: String {
var array: [String] = []
if !self.street1.isEmpty {
array.append(self.street1)
}
if !self.city.isEmpty {
array.append(self.city)
}
if !self.state.isEmpty {
array.append(self.state)
}
if !self.country.isEmpty {
array.append(self.country)
}
return array.joined(separator: ", ")
}
}
@@ -0,0 +1,22 @@
import Foundation
import TelegramCore
import TelegramUIPreferences
import SwiftSignalKit
public typealias DeviceContactStableId = String
public var sharedDisableDeviceContactDataDiffing: Bool = false
public protocol DeviceContactDataManager: AnyObject {
func personNameDisplayOrder() -> Signal<PresentationPersonNameOrder, NoError>
func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError>
func basicDataForNormalizedPhoneNumber(_ normalizedNumber: DeviceContactNormalizedPhoneNumber) -> Signal<[(DeviceContactStableId, DeviceContactBasicData)], NoError>
func extendedData(stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
func importable() -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError>
func appSpecificReferences() -> Signal<[EnginePeer.Id: DeviceContactBasicDataWithReference], NoError>
func search(query: String) -> Signal<[DeviceContactStableId: (DeviceContactBasicData, EnginePeer.Id?)], NoError>
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
func createContactWithData(_ contactData: DeviceContactExtendedData) -> Signal<(DeviceContactStableId, DeviceContactExtendedData)?, NoError>
func deleteContactWithAppSpecificReference(peerId: EnginePeer.Id) -> Signal<Never, NoError>
}
@@ -0,0 +1,20 @@
import Foundation
import TelegramCore
import TelegramUIPreferences
import SwiftSignalKit
public protocol DownloadedMediaStoreManager: AnyObject {
func store(_ media: AnyMediaReference, timestamp: Int32, peerId: EnginePeer.Id)
}
public func storeDownloadedMedia(storeManager: DownloadedMediaStoreManager?, media: AnyMediaReference, peerId: EnginePeer.Id) -> Signal<Never, NoError> {
guard case let .message(message, _) = media, let timestamp = message.timestamp, let incoming = message.isIncoming, incoming, let secret = message.isSecret, !secret else {
return .complete()
}
return Signal { [weak storeManager] subscriber in
storeManager?.store(media, timestamp: timestamp, peerId: peerId)
subscriber.putCompletion()
return EmptyDisposable
}
}
@@ -0,0 +1,173 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramUIPreferences
import RangeSet
public enum FetchManagerCategory: Int32 {
case image
case file
case voice
case animation
}
public enum FetchManagerLocationKey: Comparable, Hashable {
case messageId(MessageId)
case free
public static func <(lhs: FetchManagerLocationKey, rhs: FetchManagerLocationKey) -> Bool {
switch lhs {
case let .messageId(lhsId):
if case let .messageId(rhsId) = rhs {
return lhsId < rhsId
} else {
return true
}
case .free:
if case .free = rhs {
return false
} else {
return false
}
}
}
}
public struct FetchManagerPriorityKey: Comparable {
public let locationKey: FetchManagerLocationKey
public let hasElevatedPriority: Bool
public let userInitiatedPriority: Int32?
public let topReference: FetchManagerPriority?
public init(locationKey: FetchManagerLocationKey, hasElevatedPriority: Bool, userInitiatedPriority: Int32?, topReference: FetchManagerPriority?) {
self.locationKey = locationKey
self.hasElevatedPriority = hasElevatedPriority
self.userInitiatedPriority = userInitiatedPriority
self.topReference = topReference
}
public static func <(lhs: FetchManagerPriorityKey, rhs: FetchManagerPriorityKey) -> Bool {
if let lhsUserInitiatedPriority = lhs.userInitiatedPriority, let rhsUserInitiatedPriority = rhs.userInitiatedPriority {
if lhsUserInitiatedPriority != rhsUserInitiatedPriority {
if lhsUserInitiatedPriority > rhsUserInitiatedPriority {
return false
} else {
return true
}
}
} else if (lhs.userInitiatedPriority != nil) != (rhs.userInitiatedPriority != nil) {
if lhs.userInitiatedPriority != nil {
return true
} else {
return false
}
}
if lhs.hasElevatedPriority != rhs.hasElevatedPriority {
if lhs.hasElevatedPriority {
return true
} else {
return false
}
}
if lhs.topReference != rhs.topReference {
if let lhsTopReference = lhs.topReference, let rhsTopReference = rhs.topReference {
return lhsTopReference < rhsTopReference
} else if lhs.topReference != nil {
return true
} else {
return false
}
}
return lhs.locationKey < rhs.locationKey
}
}
public enum FetchManagerLocation: Hashable, CustomStringConvertible {
case chat(PeerId)
public var description: String {
switch self {
case let .chat(peerId):
return "chat:\(peerId)"
}
}
}
public enum FetchManagerForegroundDirection {
case toEarlier
case toLater
}
public enum FetchManagerPriority: Comparable {
case userInitiated
case foregroundPrefetch(direction: FetchManagerForegroundDirection, localOrder: MessageIndex)
case backgroundPrefetch(locationOrder: HistoryPreloadIndex, localOrder: MessageIndex)
public static func <(lhs: FetchManagerPriority, rhs: FetchManagerPriority) -> Bool {
switch lhs {
case .userInitiated:
switch rhs {
case .userInitiated:
return false
case .foregroundPrefetch:
return true
case .backgroundPrefetch:
return true
}
case let .foregroundPrefetch(lhsDirection, lhsLocalOrder):
switch rhs {
case .userInitiated:
return false
case let .foregroundPrefetch(rhsDirection, rhsLocalOrder):
if lhsDirection == rhsDirection {
switch lhsDirection {
case .toEarlier:
return lhsLocalOrder > rhsLocalOrder
case .toLater:
return lhsLocalOrder < rhsLocalOrder
}
} else {
if lhsDirection == .toEarlier {
return true
} else {
return false
}
}
case .backgroundPrefetch:
return true
}
case let .backgroundPrefetch(lhsLocationOrder, lhsLocalOrder):
switch rhs {
case .userInitiated:
return false
case .foregroundPrefetch:
return false
case let .backgroundPrefetch(rhsLocationOrder, rhsLocalOrder):
if lhsLocationOrder != rhsLocationOrder {
return lhsLocationOrder < rhsLocationOrder
}
return lhsLocalOrder > rhsLocalOrder
}
}
}
}
public protocol FetchManager {
var queue: Queue { get }
func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, mediaReference: AnyMediaReference?, resourceReference: MediaResourceReference, ranges: RangeSet<Int64>, statsCategory: MediaResourceStatsCategory, elevatedPriority: Bool, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerId: EnginePeer.Id?) -> Signal<Void, NoError>
func cancelInteractiveFetches(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource)
func cancelInteractiveFetches(resourceId: String)
func toggleInteractiveFetchPaused(resourceId: String, isPaused: Bool)
func raisePriority(resourceId: String)
func fetchStatus(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource) -> Signal<MediaResourceStatus, NoError>
}
public protocol PrefetchManager {
var preloadedGreetingSticker: ChatGreetingData { get }
func prepareNextGreetingSticker()
}
@@ -0,0 +1,107 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramUIPreferences
import RangeSet
public func freeMediaFileInteractiveFetched(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference) -> Signal<FetchResourceSourceType, FetchResourceError> {
return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource))
}
public func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileReference: FileMediaReference, priority: FetchManagerPriority) -> Signal<Void, NoError> {
let file = fileReference.media
let mediaReference = AnyMediaReference.standalone(media: fileReference.media)
return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: RangeSet<Int64>(0 ..< Int64.max), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerId: nil)
}
public func freeMediaFileResourceInteractiveFetched(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, resource: MediaResource) -> Signal<FetchResourceSourceType, FetchResourceError> {
return fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource))
}
public func freeMediaFileResourceInteractiveFetched(postbox: Postbox, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, resource: MediaResource, range: (Range<Int64>, MediaBoxFetchPriority)? = nil) -> Signal<FetchResourceSourceType, FetchResourceError> {
return fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(resource), range: range)
}
public func cancelFreeMediaFileInteractiveFetch(account: Account, file: TelegramMediaFile) {
account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource)
}
private func fetchCategoryForFile(_ file: TelegramMediaFile) -> FetchManagerCategory {
if file.isVoice || file.isInstantVideo {
return .voice
} else if file.isAnimated {
return .animation
} else {
return .file
}
}
public func messageMediaFileInteractiveFetched(context: AccountContext, message: Message, file: TelegramMediaFile, userInitiated: Bool, storeToDownloadsPeerId: EnginePeer.Id? = nil) -> Signal<Void, NoError> {
return messageMediaFileInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), file: file, userInitiated: userInitiated, priority: .userInitiated, storeToDownloadsPeerId: storeToDownloadsPeerId)
}
public func messageMediaFileInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, file: TelegramMediaFile, ranges: RangeSet<Int64> = RangeSet<Int64>(0 ..< Int64.max), userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerId: EnginePeer.Id? = nil) -> Signal<Void, NoError> {
let mediaReference = AnyMediaReference.message(message: messageReference, media: file)
return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: ranges, statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerId: storeToDownloadsPeerId)
}
public func messageMediaFileCancelInteractiveFetch(context: AccountContext, messageId: MessageId, file: TelegramMediaFile) {
context.fetchManager.cancelInteractiveFetches(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource)
}
public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, range: Range<Int64>? = nil, userInitiated: Bool = true, storeToDownloadsPeerId: EnginePeer.Id?) -> Signal<Void, NoError> {
return messageMediaImageInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, range: range, userInitiated: userInitiated, priority: .userInitiated, storeToDownloadsPeerId: storeToDownloadsPeerId)
}
public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, range: Range<Int64>? = nil, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerId: EnginePeer.Id?) -> Signal<Void, NoError> {
let mediaReference = AnyMediaReference.message(message: messageReference, media: image)
let ranges: RangeSet<Int64>
if let range = range {
ranges = RangeSet(range.lowerBound ..< range.upperBound)
} else {
ranges = RangeSet(0 ..< Int64.max)
}
return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: ranges, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerId: storeToDownloadsPeerId)
}
public func messageMediaImageCancelInteractiveFetch(context: AccountContext, messageId: MessageId, image: TelegramMediaImage, resource: MediaResource) {
context.fetchManager.cancelInteractiveFetches(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: resource)
}
public func messageMediaFileStatus(context: AccountContext, messageId: MessageId, file: TelegramMediaFile, adjustForVideoThumbnail: Bool = false) -> Signal<MediaResourceStatus, NoError> {
let fileStatus = context.fetchManager.fetchStatus(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource)
if !adjustForVideoThumbnail {
return fileStatus
}
var thumbnailStatus: Signal<MediaResourceStatus?, NoError> = .single(nil)
if let videoThumbnail = file.videoThumbnails.first {
thumbnailStatus = context.account.postbox.mediaBox.resourceStatus(videoThumbnail.resource)
|> map(Optional.init)
}
return combineLatest(queue: context.fetchManager.queue,
fileStatus,
thumbnailStatus
)
|> map { fileStatus, thumbnailStatus -> MediaResourceStatus in
guard let thumbnailStatus = thumbnailStatus else {
return fileStatus
}
if case .Local = thumbnailStatus {
return thumbnailStatus
} else {
return fileStatus
}
}
|> distinctUntilChanged
}
public func messageMediaImageStatus(context: AccountContext, messageId: MessageId, image: TelegramMediaImage) -> Signal<MediaResourceStatus, NoError> {
guard let representation = image.representations.last else {
return .single(.Remote(progress: 0.0))
}
return context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: representation.resource)
}
@@ -0,0 +1,64 @@
import Foundation
import UIKit
import Display
import Postbox
import SwiftSignalKit
import TelegramCore
public enum GalleryControllerItemSource {
case peerMessagesAtId(messageId: MessageId, chatLocation: ChatLocation, customTag: MemoryBuffer?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>)
case standaloneMessage(Message, Int?)
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, loadMore: (() -> Void)?)
}
public final class GalleryControllerActionInteraction {
public let openUrl: (String, Bool, Bool) -> Void
public let openUrlIn: (String) -> Void
public let openPeerMention: (String) -> Void
public let openPeer: (EnginePeer) -> Void
public let openHashtag: (String?, String) -> Void
public let openBotCommand: (String) -> Void
public let openAd: (MessageId) -> Void
public let addContact: (String) -> Void
public let storeMediaPlaybackState: (MessageId, Double?, Double) -> Void
public let editMedia: (MessageId, [UIView], @escaping () -> Void) -> Void
public let updateCanReadHistory: (Bool) -> Void
public init(
openUrl: @escaping (String, Bool, Bool) -> Void,
openUrlIn: @escaping (String) -> Void,
openPeerMention: @escaping (String) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openHashtag: @escaping (String?, String) -> Void,
openBotCommand: @escaping (String) -> Void,
openAd: @escaping (MessageId) -> Void,
addContact: @escaping (String) -> Void,
storeMediaPlaybackState: @escaping (MessageId, Double?, Double) -> Void,
editMedia: @escaping (MessageId, [UIView], @escaping () -> Void) -> Void,
updateCanReadHistory: @escaping (Bool) -> Void)
{
self.openUrl = openUrl
self.openUrlIn = openUrlIn
self.openPeerMention = openPeerMention
self.openPeer = openPeer
self.openHashtag = openHashtag
self.openBotCommand = openBotCommand
self.openAd = openAd
self.addContact = addContact
self.storeMediaPlaybackState = storeMediaPlaybackState
self.editMedia = editMedia
self.updateCanReadHistory = updateCanReadHistory
}
}
public protocol GalleryControllerProtocol: ViewController {
}
public protocol StickerPackScreen {
}
public protocol StickerPickerInput {
}
@@ -0,0 +1,6 @@
import Foundation
public struct GlobalExperimentalSettings {
public static var isAppStoreBuild: Bool = false
public static var enableFeed: Bool = false
}
@@ -0,0 +1,60 @@
import Foundation
import Postbox
import TelegramCore
private let minimalStreamableSize: Int = 384 * 1024
public func isMediaStreamable(message: Message, media: TelegramMediaFile) -> Bool {
if message.containsSecretMedia {
return false
}
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
return false
}
guard let size = media.size else {
return false
}
if size < minimalStreamableSize {
return false
}
for attribute in media.attributes {
if case let .Video(_, _, flags, _, _, _) = attribute {
if flags.contains(.supportsStreaming) || !media.alternativeRepresentations.isEmpty {
return true
}
break
}
}
#if DEBUG
if let fileName = media.fileName, fileName.hasSuffix(".mkv") {
return true
}
#endif
return false
}
public func isMediaStreamable(media: TelegramMediaFile) -> Bool {
guard let size = media.size else {
return false
}
if size < minimalStreamableSize {
return false
}
for attribute in media.attributes {
if case let .Video(_, _, flags, _, _, _) = attribute {
if flags.contains(.supportsStreaming) {
return true
}
break
}
}
return false
}
public func isMediaStreamable(resource: MediaResource) -> Bool {
if let size = resource.size, size >= minimalStreamableSize {
return true
} else {
return false
}
}
@@ -0,0 +1,18 @@
import Foundation
import TelegramCore
import SwiftSignalKit
public protocol LiveLocationSummaryManager {
func broadcastingToMessages() -> Signal<[EngineMessage.Id: EngineMessage], NoError>
func peersBroadcastingTo(peerId: EnginePeer.Id) -> Signal<[(EnginePeer, EngineMessage)]?, NoError>
}
public protocol LiveLocationManager {
var summaryManager: LiveLocationSummaryManager { get }
var isPolling: Signal<Bool, NoError> { get }
var hasBackgroundTasks: Signal<Bool, NoError> { get }
func cancelLiveLocation(peerId: EnginePeer.Id)
func pollOnce()
func internalMessageForPeerId(_ peerId: EnginePeer.Id) -> EngineMessage.Id?
}
@@ -0,0 +1,310 @@
import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import UIKit
import AsyncDisplayKit
import TelegramAudio
import UniversalMediaPlayer
import RangeSet
public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId {
case peer(PeerId)
case recentActions(PeerId)
case feed(Int32)
case savedMusic(PeerId)
case custom
public func isEqual(to: SharedMediaPlaylistId) -> Bool {
if let to = to as? PeerMessagesMediaPlaylistId {
return self == to
}
return false
}
}
public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation {
case messages(chatLocation: ChatLocation, tagMask: MessageTags, at: MessageId)
case singleMessage(MessageId)
case recentActions(Message)
case savedMusic(context: ProfileSavedMusicContext, at: Int32, canReorder: Bool)
case custom(messages: Signal<([Message], Int32, Bool), NoError>, canReorder: Bool, at: MessageId, loadMore: (() -> Void)?)
public var playlistId: PeerMessagesMediaPlaylistId {
switch self {
case let .messages(chatLocation, _, _):
switch chatLocation {
case let .peer(peerId):
return .peer(peerId)
case let .replyThread(replyThreaMessage):
return .peer(replyThreaMessage.peerId)
case .customChatContents:
return .custom
}
case let .singleMessage(id):
return .peer(id.peerId)
case let .recentActions(message):
return .recentActions(message.id.peerId)
case let .savedMusic(context, _, _):
return .savedMusic(context.peerId)
case .custom:
return .custom
}
}
public func effectiveLocation(context: AccountContext) -> PeerMessagesPlaylistLocation {
switch self {
case let .savedMusic(savedMusicContext, at, canReorder):
let peerId = savedMusicContext.peerId
return .custom(
messages: combineLatest(
savedMusicContext.state,
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: savedMusicContext.peerId))
)
|> map { state, peer in
var messages: [Message] = []
var peers = SimpleDictionary<PeerId, Peer>()
if let peer {
peers[peerId] = peer._asPeer()
}
for file in state.files {
let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max))
messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]))
}
var canLoadMore = false
if case let .ready(canLoadMoreValue) = state.dataState {
canLoadMore = canLoadMoreValue
}
return (messages, Int32(messages.count), canLoadMore)
},
canReorder: canReorder,
at: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: at),
loadMore: { [weak savedMusicContext] in
guard let savedMusicContext else {
return
}
savedMusicContext.loadMore()
}
)
default:
return self
}
}
public var messageId: MessageId? {
switch self {
case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, _, messageId, _):
return messageId
default:
return nil
}
}
public func isEqual(to: SharedMediaPlaylistLocation) -> Bool {
if let to = to as? PeerMessagesPlaylistLocation {
return self == to
} else {
return false
}
}
public static func ==(lhs: PeerMessagesPlaylistLocation, rhs: PeerMessagesPlaylistLocation) -> Bool {
switch lhs {
case let .messages(chatLocation, tagMask, at):
if case .messages(chatLocation, tagMask, at) = rhs {
return true
} else {
return false
}
case let .singleMessage(messageId):
if case .singleMessage(messageId) = rhs {
return true
} else {
return false
}
case let .recentActions(lhsMessage):
if case let .recentActions(rhsMessage) = rhs, lhsMessage.id == rhsMessage.id {
return true
} else {
return false
}
case let .savedMusic(lhsContext, lhsAt, _):
if case let .savedMusic(rhsContext, rhsAt, _) = rhs {
return lhsContext.peerId == rhsContext.peerId && lhsAt == rhsAt
} else {
return false
}
case let .custom(_, _, lhsAt, _):
if case let .custom(_, _, rhsAt, _) = rhs, lhsAt == rhsAt {
return true
} else {
return false
}
}
}
}
public func peerMessageMediaPlayerType(_ message: EngineMessage) -> MediaManagerPlayerType? {
func extractFileMedia(_ message: EngineMessage) -> TelegramMediaFile? {
var file: TelegramMediaFile?
for media in message.media {
if let media = media as? TelegramMediaFile {
file = media
break
} else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content, let f = content.file {
file = f
break
}
}
return file
}
if let file = extractFileMedia(message) {
if file.isVoice || file.isInstantVideo {
return .voice
} else if file.isMusic {
return .music
}
}
return nil
}
public func peerMessagesMediaPlaylistAndItemId(_ message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool, isSavedMusic: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
if isSavedMusic {
return (PeerMessagesMediaPlaylistId.savedMusic(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
} else if isGlobalSearch && !isDownloadList {
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
} else if isRecentActions && !isDownloadList {
return (PeerMessagesMediaPlaylistId.recentActions(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
} else {
return (PeerMessagesMediaPlaylistId.peer(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
}
}
public enum MediaManagerPlayerType {
case voice
case music
case file
}
public struct AudioRecorderResumeData {
public let compressedData: Data
public let resumeData: Data
public init(compressedData: Data, resumeData: Data) {
self.compressedData = compressedData
self.resumeData = resumeData
}
}
public protocol MediaManager: AnyObject {
var audioSession: ManagedAudioSession { get }
var galleryHiddenMediaManager: GalleryHiddenMediaManager { get }
var universalVideoManager: UniversalVideoManager { get }
var overlayMediaManager: OverlayMediaManager { get }
var currentPictureInPictureNode: AnyObject? { get set }
var globalMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> { get }
var musicMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> { get }
var activeGlobalMediaPlayerAccountId: Signal<(AccountRecordId, Bool)?, NoError> { get }
func setPlaylist(_ playlist: (AccountContext, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction)
func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType?)
func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal<SharedMediaPlayerItemPlaybackState?, NoError>
func filteredPlayerAudioLevelEvents(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal<Float, NoError>
func setOverlayVideoNode(_ node: OverlayMediaItemNode?)
func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool
func audioRecorder(
resumeData: AudioRecorderResumeData?,
beginWithTone: Bool,
applicationBindings: TelegramApplicationBindings,
beganWithTone: @escaping (Bool) -> Void
) -> Signal<ManagedAudioRecorder?, NoError>
}
public enum GalleryHiddenMediaId: Hashable {
case chat(AccountRecordId, MessageId, Media)
public static func ==(lhs: GalleryHiddenMediaId, rhs: GalleryHiddenMediaId) -> Bool {
switch lhs {
case let .chat(lhsAccountId ,lhsMessageId, lhsMedia):
if case let .chat(rhsAccountId, rhsMessageId, rhsMedia) = rhs, lhsAccountId == rhsAccountId, lhsMessageId == rhsMessageId, lhsMedia.isEqual(to: rhsMedia) {
return true
} else {
return false
}
}
}
public func hash(into hasher: inout Hasher) {
switch self {
case let .chat(accountId, messageId, _):
hasher.combine(accountId)
hasher.combine(messageId)
}
}
}
public protocol GalleryHiddenMediaTarget: AnyObject {
func getTransitionInfo(messageId: MessageId, media: Media) -> ((UIView) -> Void, ASDisplayNode, () -> (UIView?, UIView?))?
}
public protocol GalleryHiddenMediaManager: AnyObject {
func hiddenIds() -> Signal<Set<GalleryHiddenMediaId>, NoError>
func addSource(_ signal: Signal<GalleryHiddenMediaId?, NoError>) -> Int
func removeSource(_ index: Int)
func addTarget(_ target: GalleryHiddenMediaTarget)
func removeTarget(_ target: GalleryHiddenMediaTarget)
func findTarget(messageId: MessageId, media: Media) -> ((UIView) -> Void, ASDisplayNode, () -> (UIView?, UIView?))?
}
public protocol UniversalVideoManager: AnyObject {
func attachUniversalVideoContent(content: UniversalVideoContent, priority: UniversalVideoPriority, create: () -> UniversalVideoContentNode & ASDisplayNode, update: @escaping (((UniversalVideoContentNode & ASDisplayNode), Bool)?) -> Void) -> (AnyHashable, Int32)
func detachUniversalVideoContent(id: AnyHashable, index: Int32)
func withUniversalVideoContent(id: AnyHashable, _ f: ((UniversalVideoContentNode & ASDisplayNode)?) -> Void)
func addPlaybackCompleted(id: AnyHashable, _ f: @escaping () -> Void) -> Int
func removePlaybackCompleted(id: AnyHashable, index: Int)
func statusSignal(content: UniversalVideoContent) -> Signal<MediaPlayerStatus?, NoError>
func bufferingStatusSignal(content: UniversalVideoContent) -> Signal<(RangeSet<Int64>, Int64)?, NoError>
func isNativePictureInPictureActiveSignal(content: UniversalVideoContent) -> Signal<Bool, NoError>
}
public enum AudioRecordingState: Equatable {
case paused(duration: Double)
case recording(duration: Double, durationMediaTimestamp: Double)
case stopped
}
public struct RecordedAudioData {
public let compressedData: Data
public let resumeData: Data?
public let duration: Double
public let waveform: Data?
public let trimRange: Range<Double>?
public init(compressedData: Data, resumeData: Data?, duration: Double, waveform: Data?, trimRange: Range<Double>?) {
self.compressedData = compressedData
self.resumeData = resumeData
self.duration = duration
self.waveform = waveform
self.trimRange = trimRange
}
}
public protocol ManagedAudioRecorder: AnyObject {
var beginWithTone: Bool { get }
var micLevel: Signal<Float, NoError> { get }
var recordingState: Signal<AudioRecordingState, NoError> { get }
func start()
func pause()
func resume()
func stop()
func takenRecordedData() -> Signal<RecordedAudioData?, NoError>
func updateTrimRange(start: Double, end: Double, updatedEnd: Bool, apply: Bool)
}
@@ -0,0 +1,118 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
import AsyncDisplayKit
import UniversalMediaPlayer
import TelegramPresentationData
import TextFormat
public enum ChatControllerInteractionOpenMessageMode {
case `default`
case stream
case automaticPlayback
case landscape
case timecode(Double)
case link
}
public final class OpenChatMessageParams {
public let context: AccountContext
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
public let chatLocation: ChatLocation?
public let chatFilterTag: MemoryBuffer?
public let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?
public let message: Message
public let mediaIndex: Int?
public let standalone: Bool
public let reverseMessageGalleryOrder: Bool
public let mode: ChatControllerInteractionOpenMessageMode
public let navigationController: NavigationController?
public let modal: Bool
public let dismissInput: () -> Void
public let present: (ViewController, Any?, PresentationContextType) -> Void
public let transitionNode: (MessageId, Media, Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
public let addToTransitionSurface: (UIView) -> Void
public let openUrl: (String) -> Void
public let openPeer: (Peer, ChatControllerInteractionNavigateToPeer) -> Void
public let callPeer: (PeerId, Bool) -> Void
public let openConferenceCall: (Message) -> Void
public let enqueueMessage: (EnqueueMessage) -> Void
public let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?
public let sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?
public let setupTemporaryHiddenMedia: (Signal<Any?, NoError>, Int, Media) -> Void
public let chatAvatarHiddenMedia: (Signal<MessageId?, NoError>, Media) -> Void
public let actionInteraction: GalleryControllerActionInteraction?
public let playlistLocation: PeerMessagesPlaylistLocation?
public let gallerySource: GalleryControllerItemSource?
public let centralItemUpdated: ((MessageId) -> Void)?
public let getSourceRect: (() -> CGRect?)?
public let blockInteraction: Promise<Bool>
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
chatLocation: ChatLocation?,
chatFilterTag: MemoryBuffer?,
chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?,
message: Message,
mediaIndex: Int? = nil,
standalone: Bool,
reverseMessageGalleryOrder: Bool,
mode: ChatControllerInteractionOpenMessageMode = .default,
navigationController: NavigationController?,
modal: Bool = false,
dismissInput: @escaping () -> Void,
present: @escaping (ViewController, Any?, PresentationContextType) -> Void,
transitionNode: @escaping (MessageId, Media, Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?,
addToTransitionSurface: @escaping (UIView) -> Void,
openUrl: @escaping (String) -> Void,
openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void,
callPeer: @escaping (PeerId, Bool) -> Void,
openConferenceCall: @escaping (Message) -> Void,
enqueueMessage: @escaping (EnqueueMessage) -> Void,
sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?,
sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?,
setupTemporaryHiddenMedia: @escaping (Signal<Any?, NoError>, Int, Media) -> Void,
chatAvatarHiddenMedia: @escaping (Signal<MessageId?, NoError>, Media) -> Void,
actionInteraction: GalleryControllerActionInteraction? = nil,
playlistLocation: PeerMessagesPlaylistLocation? = nil,
gallerySource: GalleryControllerItemSource? = nil,
centralItemUpdated: ((MessageId) -> Void)? = nil,
getSourceRect: (() -> CGRect?)? = nil
) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.chatLocation = chatLocation
self.chatFilterTag = chatFilterTag
self.chatLocationContextHolder = chatLocationContextHolder
self.message = message
self.mediaIndex = mediaIndex
self.standalone = standalone
self.reverseMessageGalleryOrder = reverseMessageGalleryOrder
self.mode = mode
self.navigationController = navigationController
self.modal = modal
self.dismissInput = dismissInput
self.present = present
self.transitionNode = transitionNode
self.addToTransitionSurface = addToTransitionSurface
self.openUrl = openUrl
self.openPeer = openPeer
self.callPeer = callPeer
self.openConferenceCall = openConferenceCall
self.enqueueMessage = enqueueMessage
self.sendSticker = sendSticker
self.sendEmoji = sendEmoji
self.setupTemporaryHiddenMedia = setupTemporaryHiddenMedia
self.chatAvatarHiddenMedia = chatAvatarHiddenMedia
self.actionInteraction = actionInteraction
self.playlistLocation = playlistLocation
self.gallerySource = gallerySource
self.centralItemUpdated = centralItemUpdated
self.getSourceRect = getSourceRect
self.blockInteraction = Promise()
}
}
@@ -0,0 +1,5 @@
import Foundation
public protocol OverlayAudioPlayerController: AnyObject {
}
@@ -0,0 +1,71 @@
import Foundation
import UIKit
import AsyncDisplayKit
import AVKit
public struct OverlayMediaItemNodeGroup: Hashable, RawRepresentable {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
}
public enum OverlayMediaItemMinimizationEdge {
case left
case right
}
open class OverlayMediaItemNode: ASDisplayNode {
open var hasAttachedContextUpdated: ((Bool) -> Void)?
open var hasAttachedContext: Bool = false
open var unminimize: (() -> Void)?
public var manualExpandEmbed: (() -> Void)?
public var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
open var group: OverlayMediaItemNodeGroup? {
return nil
}
open var tempExtendedTopInset: Bool {
return false
}
open var isMinimizeable: Bool {
return false
}
open var customTransition: Bool = false
open var isRemoved: Bool = false
open func setShouldAcquireContext(_ value: Bool) {
}
open func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
return CGSize(width: 50.0, height: 50.0)
}
open func updateLayout(_ size: CGSize) {
}
open func dismiss() {
}
open func updateMinimizedEdge(_ edge: OverlayMediaItemMinimizationEdge?, adjusting: Bool) {
}
open func performCustomTransitionIn() -> Bool {
return false
}
open func performCustomTransitionOut() -> Bool {
return false
}
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
open func makeNativeContentSource() -> AVPictureInPictureController.ContentSource? {
return nil
}
}
@@ -0,0 +1,57 @@
import Foundation
import UIKit
import Display
import AVFoundation
import AsyncDisplayKit
public final class OverlayMediaControllerEmbeddingItem {
public let position: CGPoint
public let itemNode: OverlayMediaItemNode
public init(
position: CGPoint,
itemNode: OverlayMediaItemNode
) {
self.position = position
self.itemNode = itemNode
}
}
public protocol PictureInPictureContent: AnyObject {
var videoNode: ASDisplayNode { get }
}
public protocol OverlayMediaController: AnyObject {
var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)? { get set }
var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)? { get set }
var hasNodes: Bool { get }
func addNode(_ node: OverlayMediaItemNode, customTransition: Bool)
func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool)
func setPictureInPictureContent(content: PictureInPictureContent, absoluteRect: CGRect)
func setPictureInPictureContentHidden(content: PictureInPictureContent, isHidden value: Bool)
func removePictureInPictureContent(content: PictureInPictureContent)
}
public final class OverlayMediaManager {
public var controller: (OverlayMediaController & ViewController)?
public var updatePossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem?) -> Void)?
public var embedPossibleEmbeddingItem: ((OverlayMediaControllerEmbeddingItem) -> Bool)?
public init() {
}
public func attachOverlayMediaController(_ controller: OverlayMediaController & ViewController) {
self.controller = controller
controller.updatePossibleEmbeddingItem = { [weak self] item in
self?.updatePossibleEmbeddingItem?(item)
}
controller.embedPossibleEmbeddingItem = { [weak self] item in
return self?.embedPossibleEmbeddingItem?(item) ?? false
}
}
}
@@ -0,0 +1,363 @@
import Foundation
import UIKit
import TelegramCore
private extension PeerNameColors.Colors {
init?(colors: EngineAvailableColorOptions.MultiColorPack) {
if colors.colors.isEmpty {
return nil
}
self.main = UIColor(rgb: colors.colors[0])
if colors.colors.count > 1 {
self.secondary = UIColor(rgb: colors.colors[1])
} else {
self.secondary = nil
}
if colors.colors.count > 2 {
self.tertiary = UIColor(rgb: colors.colors[2])
} else {
self.tertiary = nil
}
}
}
public class PeerNameColors: Equatable {
public enum Subject {
case background
case palette
case stories
}
public struct Colors: Equatable {
public let main: UIColor
public let secondary: UIColor?
public let tertiary: UIColor?
public init(main: UIColor, secondary: UIColor?, tertiary: UIColor?) {
self.main = main
self.secondary = secondary
self.tertiary = tertiary
}
public init(main: UIColor) {
self.main = main
self.secondary = nil
self.tertiary = nil
}
public init?(colors: [UIColor]) {
guard let first = colors.first else {
return nil
}
self.main = first
if colors.count == 3 {
self.secondary = colors[1]
self.tertiary = colors[2]
} else if colors.count == 2, let second = colors.last {
self.secondary = second
self.tertiary = nil
} else {
self.secondary = nil
self.tertiary = nil
}
}
}
public static var defaultSingleColors: [Int32: Colors] {
return [
0: Colors(main: UIColor(rgb: 0xcc5049)),
1: Colors(main: UIColor(rgb: 0xd67722)),
2: Colors(main: UIColor(rgb: 0x955cdb)),
3: Colors(main: UIColor(rgb: 0x40a920)),
4: Colors(main: UIColor(rgb: 0x309eba)),
5: Colors(main: UIColor(rgb: 0x368ad1)),
6: Colors(main: UIColor(rgb: 0xc7508b))
]
}
public static var defaultValue: PeerNameColors {
return PeerNameColors(
colors: defaultSingleColors,
darkColors: [:],
displayOrder: [5, 3, 1, 0, 2, 4, 6],
chatFolderTagDisplayOrder: [5, 3, 1, 0, 2, 4, 6],
profileColors: [:],
profileDarkColors: [:],
profilePaletteColors: [:],
profilePaletteDarkColors: [:],
profileStoryColors: [:],
profileStoryDarkColors: [:],
profileDisplayOrder: [],
nameColorsChannelMinRequiredBoostLevel: [:],
profileColorsChannelMinRequiredBoostLevel: [:],
profileColorsGroupMinRequiredBoostLevel: [:]
)
}
public let colors: [Int32: Colors]
public let darkColors: [Int32: Colors]
public let displayOrder: [Int32]
public let chatFolderTagDisplayOrder: [Int32]
public let profileColors: [Int32: Colors]
public let profileDarkColors: [Int32: Colors]
public let profilePaletteColors: [Int32: Colors]
public let profilePaletteDarkColors: [Int32: Colors]
public let profileStoryColors: [Int32: Colors]
public let profileStoryDarkColors: [Int32: Colors]
public let profileDisplayOrder: [Int32]
public let nameColorsChannelMinRequiredBoostLevel: [Int32: Int32]
public let profileColorsChannelMinRequiredBoostLevel: [Int32: Int32]
public let profileColorsGroupMinRequiredBoostLevel: [Int32: Int32]
public func get(_ color: PeerNameColor, dark: Bool = false) -> Colors {
if dark, let colors = self.darkColors[color.rawValue] {
return colors
} else if let colors = self.colors[color.rawValue] {
return colors
} else {
return PeerNameColors.defaultSingleColors[5]!
}
}
public func getChatFolderTag(_ color: PeerNameColor, dark: Bool = false) -> Colors {
if dark, let colors = self.darkColors[color.rawValue] {
return colors
} else if let colors = self.colors[color.rawValue] {
return colors
} else {
return PeerNameColors.defaultSingleColors[5]!
}
}
public func getProfile(_ color: PeerNameColor, dark: Bool = false, subject: Subject = .background) -> Colors {
switch subject {
case .background:
if dark, let colors = self.profileDarkColors[color.rawValue] {
return colors
} else if let colors = self.profileColors[color.rawValue] {
return colors
} else {
return Colors(main: UIColor(rgb: 0xcc5049))
}
case .palette:
if dark, let colors = self.profilePaletteDarkColors[color.rawValue] {
return colors
} else if let colors = self.profilePaletteColors[color.rawValue] {
return colors
} else {
return self.getProfile(color, dark: dark, subject: .background)
}
case .stories:
if dark, let colors = self.profileStoryDarkColors[color.rawValue] {
return colors
} else if let colors = self.profileStoryColors[color.rawValue] {
return colors
} else {
return self.getProfile(color, dark: dark, subject: .background)
}
}
}
fileprivate init(
colors: [Int32: Colors],
darkColors: [Int32: Colors],
displayOrder: [Int32],
chatFolderTagDisplayOrder: [Int32],
profileColors: [Int32: Colors],
profileDarkColors: [Int32: Colors],
profilePaletteColors: [Int32: Colors],
profilePaletteDarkColors: [Int32: Colors],
profileStoryColors: [Int32: Colors],
profileStoryDarkColors: [Int32: Colors],
profileDisplayOrder: [Int32],
nameColorsChannelMinRequiredBoostLevel: [Int32: Int32],
profileColorsChannelMinRequiredBoostLevel: [Int32: Int32],
profileColorsGroupMinRequiredBoostLevel: [Int32: Int32]
) {
self.colors = colors
self.darkColors = darkColors
self.displayOrder = displayOrder
self.chatFolderTagDisplayOrder = chatFolderTagDisplayOrder
self.profileColors = profileColors
self.profileDarkColors = profileDarkColors
self.profilePaletteColors = profilePaletteColors
self.profilePaletteDarkColors = profilePaletteDarkColors
self.profileStoryColors = profileStoryColors
self.profileStoryDarkColors = profileStoryDarkColors
self.profileDisplayOrder = profileDisplayOrder
self.nameColorsChannelMinRequiredBoostLevel = nameColorsChannelMinRequiredBoostLevel
self.profileColorsChannelMinRequiredBoostLevel = profileColorsChannelMinRequiredBoostLevel
self.profileColorsGroupMinRequiredBoostLevel = profileColorsGroupMinRequiredBoostLevel
}
public static func with(availableReplyColors: EngineAvailableColorOptions, availableProfileColors: EngineAvailableColorOptions) -> PeerNameColors {
var colors: [Int32: Colors] = [:]
var darkColors: [Int32: Colors] = [:]
var displayOrder: [Int32] = []
var profileColors: [Int32: Colors] = [:]
var profileDarkColors: [Int32: Colors] = [:]
var profilePaletteColors: [Int32: Colors] = [:]
var profilePaletteDarkColors: [Int32: Colors] = [:]
var profileStoryColors: [Int32: Colors] = [:]
var profileStoryDarkColors: [Int32: Colors] = [:]
var profileDisplayOrder: [Int32] = []
var nameColorsChannelMinRequiredBoostLevel: [Int32: Int32] = [:]
var profileColorsChannelMinRequiredBoostLevel: [Int32: Int32] = [:]
var profileColorsGroupMinRequiredBoostLevel: [Int32: Int32] = [:]
if !availableReplyColors.options.isEmpty {
for option in availableReplyColors.options {
if let requiredChannelMinBoostLevel = option.value.requiredChannelMinBoostLevel {
nameColorsChannelMinRequiredBoostLevel[option.key] = requiredChannelMinBoostLevel
}
if let parsedLight = PeerNameColors.Colors(colors: option.value.light.background) {
colors[option.key] = parsedLight
}
if let parsedDark = (option.value.dark?.background).flatMap(PeerNameColors.Colors.init(colors:)) {
darkColors[option.key] = parsedDark
}
for option in availableReplyColors.options {
if !displayOrder.contains(option.key) {
displayOrder.append(option.key)
}
}
}
} else {
let defaultValue = PeerNameColors.defaultValue
colors = defaultValue.colors
darkColors = defaultValue.darkColors
displayOrder = defaultValue.displayOrder
}
if !availableProfileColors.options.isEmpty {
for option in availableProfileColors.options {
if let requiredChannelMinBoostLevel = option.value.requiredChannelMinBoostLevel {
profileColorsChannelMinRequiredBoostLevel[option.key] = requiredChannelMinBoostLevel
}
if let requiredGroupMinBoostLevel = option.value.requiredGroupMinBoostLevel {
profileColorsGroupMinRequiredBoostLevel[option.key] = requiredGroupMinBoostLevel
}
if let parsedLight = PeerNameColors.Colors(colors: option.value.light.background) {
profileColors[option.key] = parsedLight
}
if let parsedDark = (option.value.dark?.background).flatMap(PeerNameColors.Colors.init(colors:)) {
profileDarkColors[option.key] = parsedDark
}
if let parsedPaletteLight = PeerNameColors.Colors(colors: option.value.light.palette) {
profilePaletteColors[option.key] = parsedPaletteLight
}
if let parsedPaletteDark = (option.value.dark?.palette).flatMap(PeerNameColors.Colors.init(colors:)) {
profilePaletteDarkColors[option.key] = parsedPaletteDark
}
if let parsedStoryLight = (option.value.light.stories).flatMap(PeerNameColors.Colors.init(colors:)) {
profileStoryColors[option.key] = parsedStoryLight
}
if let parsedStoryDark = (option.value.dark?.stories).flatMap(PeerNameColors.Colors.init(colors:)) {
profileStoryDarkColors[option.key] = parsedStoryDark
}
for option in availableProfileColors.options {
if !profileDisplayOrder.contains(option.key) {
profileDisplayOrder.append(option.key)
}
}
}
}
return PeerNameColors(
colors: colors,
darkColors: darkColors,
displayOrder: displayOrder,
chatFolderTagDisplayOrder: PeerNameColors.defaultValue.chatFolderTagDisplayOrder,
profileColors: profileColors,
profileDarkColors: profileDarkColors,
profilePaletteColors: profilePaletteColors,
profilePaletteDarkColors: profilePaletteDarkColors,
profileStoryColors: profileStoryColors,
profileStoryDarkColors: profileStoryDarkColors,
profileDisplayOrder: profileDisplayOrder,
nameColorsChannelMinRequiredBoostLevel: nameColorsChannelMinRequiredBoostLevel,
profileColorsChannelMinRequiredBoostLevel: profileColorsChannelMinRequiredBoostLevel,
profileColorsGroupMinRequiredBoostLevel: profileColorsGroupMinRequiredBoostLevel
)
}
public static func == (lhs: PeerNameColors, rhs: PeerNameColors) -> Bool {
if lhs.colors != rhs.colors {
return false
}
if lhs.darkColors != rhs.darkColors {
return false
}
if lhs.displayOrder != rhs.displayOrder {
return false
}
if lhs.chatFolderTagDisplayOrder != rhs.chatFolderTagDisplayOrder {
return false
}
if lhs.profileColors != rhs.profileColors {
return false
}
if lhs.profileDarkColors != rhs.profileDarkColors {
return false
}
if lhs.profilePaletteColors != rhs.profilePaletteColors {
return false
}
if lhs.profilePaletteDarkColors != rhs.profilePaletteDarkColors {
return false
}
if lhs.profileStoryColors != rhs.profileStoryColors {
return false
}
if lhs.profileStoryDarkColors != rhs.profileStoryDarkColors {
return false
}
if lhs.profileDisplayOrder != rhs.profileDisplayOrder {
return false
}
return true
}
}
public extension PeerCollectibleColor {
func peerNameColors(dark: Bool) -> PeerNameColors.Colors {
return PeerNameColors.Colors(
main: self.mainColor(dark: dark),
secondary: self.secondaryColor(dark: dark),
tertiary: self.tertiaryColor(dark: dark)
)
}
func mainColor(dark: Bool) -> UIColor {
if dark, let darkAccentColor = self.darkAccentColor {
return UIColor(rgb: darkAccentColor)
} else {
return UIColor(rgb: self.accentColor)
}
}
func secondaryColor(dark: Bool) -> UIColor? {
if dark, let darkColors = self.darkColors, darkColors.count > 0 {
return UIColor(rgb: darkColors[0])
} else if self.colors.count > 0 {
return UIColor(rgb: self.colors[0])
} else {
return nil
}
}
func tertiaryColor(dark: Bool) -> UIColor? {
if dark, let darkColors = self.darkColors, darkColors.count > 1 {
return UIColor(rgb: darkColors[1])
} else if self.colors.count > 1 {
return UIColor(rgb: self.colors[1])
} else {
return nil
}
}
}
@@ -0,0 +1,174 @@
import Foundation
import Display
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import AnimationCache
import MultiAnimationRenderer
public struct ChatListNodePeersFilter: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let onlyWriteable = ChatListNodePeersFilter(rawValue: 1 << 0)
public static let onlyPrivateChats = ChatListNodePeersFilter(rawValue: 1 << 1)
public static let onlyGroups = ChatListNodePeersFilter(rawValue: 1 << 2)
public static let onlyChannels = ChatListNodePeersFilter(rawValue: 1 << 3)
public static let onlyManageable = ChatListNodePeersFilter(rawValue: 1 << 4)
public static let excludeSecretChats = ChatListNodePeersFilter(rawValue: 1 << 5)
public static let excludeRecent = ChatListNodePeersFilter(rawValue: 1 << 6)
public static let excludeSavedMessages = ChatListNodePeersFilter(rawValue: 1 << 7)
public static let doNotSearchMessages = ChatListNodePeersFilter(rawValue: 1 << 8)
public static let removeSearchHeader = ChatListNodePeersFilter(rawValue: 1 << 9)
public static let excludeDisabled = ChatListNodePeersFilter(rawValue: 1 << 10)
public static let excludeChannels = ChatListNodePeersFilter(rawValue: 1 << 12)
public static let onlyGroupsAndChannels = ChatListNodePeersFilter(rawValue: 1 << 13)
public static let excludeGroups = ChatListNodePeersFilter(rawValue: 1 << 14)
public static let excludeUsers = ChatListNodePeersFilter(rawValue: 1 << 15)
public static let excludeBots = ChatListNodePeersFilter(rawValue: 1 << 16)
public static let includeSelf = ChatListNodePeersFilter(rawValue: 1 << 7)
}
public enum ChatListDisabledPeerReason {
case generic
case premiumRequired
}
public final class PeerSelectionControllerParams {
public let context: AccountContext
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
public let filter: ChatListNodePeersFilter
public let requestPeerType: [ReplyMarkupButtonRequestPeerType]?
public let forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)?
public let hasFilters: Bool
public let hasChatListSelector: Bool
public let hasContactSelector: Bool
public let hasGlobalSearch: Bool
public let title: String?
public let attemptSelection: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)?
public let createNewGroup: (() -> Void)?
public let pretendPresentedInModal: Bool
public let multipleSelection: Bool
public let multipleSelectionLimit: Int32?
public let forwardedMessageIds: [EngineMessage.Id]
public let hasTypeHeaders: Bool
public let selectForumThreads: Bool
public let hasCreation: Bool
public let immediatelyActivateMultipleSelection: Bool
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
filter: ChatListNodePeersFilter = [.onlyWriteable],
requestPeerType: [ReplyMarkupButtonRequestPeerType]? = nil,
forumPeerId: (id: EnginePeer.Id, isMonoforum: Bool)? = nil,
hasFilters: Bool = false,
hasChatListSelector: Bool = true,
hasContactSelector: Bool = true,
hasGlobalSearch: Bool = true,
title: String? = nil,
attemptSelection: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)? = nil,
createNewGroup: (() -> Void)? = nil,
pretendPresentedInModal: Bool = false,
multipleSelection: Bool = false,
multipleSelectionLimit: Int32? = nil,
forwardedMessageIds: [EngineMessage.Id] = [],
hasTypeHeaders: Bool = false,
selectForumThreads: Bool = false,
hasCreation: Bool = false,
immediatelyActivateMultipleSelection: Bool = false
) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.filter = filter
self.requestPeerType = requestPeerType
self.forumPeerId = forumPeerId
self.hasFilters = hasFilters
self.hasChatListSelector = hasChatListSelector
self.hasContactSelector = hasContactSelector
self.hasGlobalSearch = hasGlobalSearch
self.title = title
self.attemptSelection = attemptSelection
self.createNewGroup = createNewGroup
self.pretendPresentedInModal = pretendPresentedInModal
self.multipleSelection = multipleSelection
self.multipleSelectionLimit = multipleSelectionLimit
self.forwardedMessageIds = forwardedMessageIds
self.hasTypeHeaders = hasTypeHeaders
self.selectForumThreads = selectForumThreads
self.hasCreation = hasCreation
self.immediatelyActivateMultipleSelection = immediatelyActivateMultipleSelection
}
}
public enum AttachmentTextInputPanelSendMode {
case generic
case silent
case schedule
case whenOnline
}
public enum PeerSelectionControllerContext {
public final class Custom {
public let accountPeerId: EnginePeer.Id
public let postbox: Postbox
public let network: Network
public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer
public let presentationData: PresentationData
public let updatedPresentationData: Signal<PresentationData, NoError>
public init(
accountPeerId: EnginePeer.Id,
postbox: Postbox,
network: Network,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
presentationData: PresentationData,
updatedPresentationData: Signal<PresentationData, NoError>
) {
self.accountPeerId = accountPeerId
self.postbox = postbox
self.network = network
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.presentationData = presentationData
self.updatedPresentationData = updatedPresentationData
}
}
case account(AccountContext)
case custom(Custom)
}
public protocol PeerSelectionController: ViewController {
var peerSelected: ((EnginePeer, Int64?) -> Void)? { get set }
var multiplePeersSelected: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.SendParameters?) -> Void)? { get set }
var inProgress: Bool { get set }
var customDismiss: (() -> Void)? { get set }
}
public enum SelectivePrivacySettingsKind {
case presence
case groupInvitations
case voiceCalls
case profilePhoto
case forwards
case phoneNumber
case voiceMessages
case bio
case birthday
case savedMusic
case giftsAutoSave
}
@@ -0,0 +1,354 @@
import Foundation
import TelegramCore
public enum PremiumIntroSource {
case settings
case stickers
case reactions
case ads
case upload
case groupsAndChannels
case pinnedChats
case publicLinks
case savedGifs
case savedStickers
case folders
case chatsPerFolder
case accounts
case appIcons
case about
case deeplink(String?)
case profile(EnginePeer.Id)
case emojiStatus(EnginePeer.Id, Int64, TelegramMediaFile?, LoadedStickerPack?)
case voiceToText
case fasterDownload
case translation
case stories
case storiesDownload
case storiesStealthMode
case storiesPermanentViews
case storiesFormatting
case storiesExpirationDurations
case storiesSuggestedReactions
case storiesHigherQuality
case storiesLinks
case channelBoost(EnginePeer.Id)
case nameColor
case similarChannels
case wallpapers
case presence
case readTime
case messageTags
case folderTags
case animatedEmoji
case messageEffects
case todo
case auth(String)
case premiumGift(TelegramMediaFile)
}
public enum PremiumGiftSource: Equatable {
case profile
case attachMenu
case settings([EnginePeer.Id: TelegramBirthday]?)
case chatList([EnginePeer.Id: TelegramBirthday]?)
case stars([EnginePeer.Id: TelegramBirthday]?)
case starGiftTransfer([EnginePeer.Id: TelegramBirthday]?, StarGiftReference, StarGift.UniqueGift, Int64, Int32?, Bool)
case channelBoost
case deeplink(String?)
}
public enum PremiumDemoSubject {
case doubleLimits
case moreUpload
case fasterDownload
case voiceToText
case noAds
case uniqueReactions
case premiumStickers
case advancedChatManagement
case profileBadge
case animatedUserpics
case appIcons
case animatedEmoji
case emojiStatus
case translation
case stories
case colors
case wallpapers
case messageTags
case lastSeen
case messagePrivacy
case folderTags
case business
case messageEffects
case todo
case businessLocation
case businessHours
case businessGreetingMessage
case businessQuickReplies
case businessAwayMessage
case businessChatBots
}
public enum PremiumLimitSubject {
case folders
case chatsPerFolder
case pins
case files
case accounts
case linksPerSharedFolder
case membershipInSharedFolders
case channels
case expiringStories
case multiStories
case storiesWeekly
case storiesMonthly
case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32, canBoostAgain: Bool)
}
public enum PremiumPrivacySubject {
case presence
case readTime
}
public enum BoostSubject: Equatable {
case stories
case channelReactions(reactionCount: Int32)
case nameColors(colors: PeerNameColor)
case nameIcon
case profileColors(colors: PeerNameColor)
case profileIcon
case emojiStatus
case wallpaper
case customWallpaper
case audioTranscription
case emojiPack
case noAds
case wearGift
case autoTranslate
}
public enum StarsPurchasePurpose: Equatable {
case generic
case topUp(requiredStars: Int64, purpose: String?)
case transfer(peerId: EnginePeer.Id, requiredStars: Int64)
case reactions(peerId: EnginePeer.Id, requiredStars: Int64)
case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool)
case gift(peerId: EnginePeer.Id)
case unlockMedia(requiredStars: Int64)
case starGift(peerId: EnginePeer.Id, requiredStars: Int64)
case upgradeStarGift(requiredStars: Int64)
case transferStarGift(requiredStars: Int64)
case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64)
case buyStarGift(requiredStars: Int64)
case removeOriginalDetailsStarGift(requiredStars: Int64)
case starGiftOffer(requiredStars: Int64)
}
public struct PremiumConfiguration {
public static var defaultValue: PremiumConfiguration {
return PremiumConfiguration(
isPremiumDisabled: false,
areStarsDisabled: true,
subscriptionManagementUrl: "",
showPremiumGiftInAttachMenu: false,
showPremiumGiftInTextField: false,
giveawayGiftsPurchaseAvailable: false,
starsGiftsPurchaseAvailable: false,
starGiftsPurchaseBlocked: true,
boostsPerGiftCount: 3,
audioTransciptionTrialMaxDuration: 300,
audioTransciptionTrialCount: 2,
minChannelNameColorLevel: 1,
minChannelNameIconLevel: 4,
minChannelProfileColorLevel: 5,
minChannelProfileIconLevel: 7,
minChannelEmojiStatusLevel: 8,
minChannelWallpaperLevel: 9,
minChannelCustomWallpaperLevel: 10,
minChannelRestrictAdsLevel: 50,
minChannelWearGiftLevel: 8,
minChannelAutoTranslateLevel: 3,
minGroupProfileIconLevel: 7,
minGroupEmojiStatusLevel: 8,
minGroupWallpaperLevel: 9,
minGroupCustomWallpaperLevel: 9,
minGroupEmojiPackLevel: 9,
minGroupAudioTranscriptionLevel: 9
)
}
public let isPremiumDisabled: Bool
public let areStarsDisabled: Bool
public let subscriptionManagementUrl: String
public let showPremiumGiftInAttachMenu: Bool
public let showPremiumGiftInTextField: Bool
public let giveawayGiftsPurchaseAvailable: Bool
public let starsGiftsPurchaseAvailable: Bool
public let starGiftsPurchaseBlocked: Bool
public let boostsPerGiftCount: Int32
public let audioTransciptionTrialMaxDuration: Int32
public let audioTransciptionTrialCount: Int32
public let minChannelNameColorLevel: Int32
public let minChannelNameIconLevel: Int32
public let minChannelProfileColorLevel: Int32
public let minChannelProfileIconLevel: Int32
public let minChannelEmojiStatusLevel: Int32
public let minChannelWallpaperLevel: Int32
public let minChannelCustomWallpaperLevel: Int32
public let minChannelRestrictAdsLevel: Int32
public let minChannelWearGiftLevel: Int32
public let minChannelAutoTranslateLevel: Int32
public let minGroupProfileIconLevel: Int32
public let minGroupEmojiStatusLevel: Int32
public let minGroupWallpaperLevel: Int32
public let minGroupCustomWallpaperLevel: Int32
public let minGroupEmojiPackLevel: Int32
public let minGroupAudioTranscriptionLevel: Int32
fileprivate init(
isPremiumDisabled: Bool,
areStarsDisabled: Bool,
subscriptionManagementUrl: String,
showPremiumGiftInAttachMenu: Bool,
showPremiumGiftInTextField: Bool,
giveawayGiftsPurchaseAvailable: Bool,
starsGiftsPurchaseAvailable: Bool,
starGiftsPurchaseBlocked: Bool,
boostsPerGiftCount: Int32,
audioTransciptionTrialMaxDuration: Int32,
audioTransciptionTrialCount: Int32,
minChannelNameColorLevel: Int32,
minChannelNameIconLevel: Int32,
minChannelProfileColorLevel: Int32,
minChannelProfileIconLevel: Int32,
minChannelEmojiStatusLevel: Int32,
minChannelWallpaperLevel: Int32,
minChannelCustomWallpaperLevel: Int32,
minChannelRestrictAdsLevel: Int32,
minChannelWearGiftLevel: Int32,
minChannelAutoTranslateLevel: Int32,
minGroupProfileIconLevel: Int32,
minGroupEmojiStatusLevel: Int32,
minGroupWallpaperLevel: Int32,
minGroupCustomWallpaperLevel: Int32,
minGroupEmojiPackLevel: Int32,
minGroupAudioTranscriptionLevel: Int32
) {
self.isPremiumDisabled = isPremiumDisabled
self.areStarsDisabled = areStarsDisabled
self.subscriptionManagementUrl = subscriptionManagementUrl
self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu
self.showPremiumGiftInTextField = showPremiumGiftInTextField
self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable
self.starsGiftsPurchaseAvailable = starsGiftsPurchaseAvailable
self.starGiftsPurchaseBlocked = starGiftsPurchaseBlocked
self.boostsPerGiftCount = boostsPerGiftCount
self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration
self.audioTransciptionTrialCount = audioTransciptionTrialCount
self.minChannelNameColorLevel = minChannelNameColorLevel
self.minChannelNameIconLevel = minChannelNameIconLevel
self.minChannelProfileColorLevel = minChannelProfileColorLevel
self.minChannelProfileIconLevel = minChannelProfileIconLevel
self.minChannelEmojiStatusLevel = minChannelEmojiStatusLevel
self.minChannelWallpaperLevel = minChannelWallpaperLevel
self.minChannelCustomWallpaperLevel = minChannelCustomWallpaperLevel
self.minChannelRestrictAdsLevel = minChannelRestrictAdsLevel
self.minChannelWearGiftLevel = minChannelWearGiftLevel
self.minChannelAutoTranslateLevel = minChannelAutoTranslateLevel
self.minGroupProfileIconLevel = minGroupProfileIconLevel
self.minGroupEmojiStatusLevel = minGroupEmojiStatusLevel
self.minGroupWallpaperLevel = minGroupWallpaperLevel
self.minGroupCustomWallpaperLevel = minGroupCustomWallpaperLevel
self.minGroupEmojiPackLevel = minGroupEmojiPackLevel
self.minGroupAudioTranscriptionLevel = minGroupAudioTranscriptionLevel
}
public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration {
let defaultValue = self.defaultValue
if let data = appConfiguration.data {
func get(_ value: Any?) -> Int32? {
return (value as? Double).flatMap(Int32.init)
}
return PremiumConfiguration(
isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? defaultValue.isPremiumDisabled,
areStarsDisabled: data["stars_purchase_blocked"] as? Bool ?? defaultValue.areStarsDisabled,
subscriptionManagementUrl: data["premium_manage_subscription_url"] as? String ?? "",
showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? defaultValue.showPremiumGiftInAttachMenu,
showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField,
giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable,
starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable,
starGiftsPurchaseBlocked: data["stargifts_blocked"] as? Bool ?? defaultValue.starGiftsPurchaseBlocked,
boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount,
audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration,
audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount,
minChannelNameColorLevel: get(data["channel_color_level_min"]) ?? defaultValue.minChannelNameColorLevel,
minChannelNameIconLevel: get(data["channel_bg_icon_level_min"]) ?? defaultValue.minChannelNameIconLevel,
minChannelProfileColorLevel: get(data["channel_profile_color_level_min"]) ?? defaultValue.minChannelProfileColorLevel,
minChannelProfileIconLevel: get(data["channel_profile_bg_icon_level_min"]) ?? defaultValue.minChannelProfileIconLevel,
minChannelEmojiStatusLevel: get(data["channel_emoji_status_level_min"]) ?? defaultValue.minChannelEmojiStatusLevel,
minChannelWallpaperLevel: get(data["channel_wallpaper_level_min"]) ?? defaultValue.minChannelWallpaperLevel,
minChannelCustomWallpaperLevel: get(data["channel_custom_wallpaper_level_min"]) ?? defaultValue.minChannelCustomWallpaperLevel,
minChannelRestrictAdsLevel: get(data["channel_restrict_sponsored_level_min"]) ?? defaultValue.minChannelRestrictAdsLevel,
minChannelWearGiftLevel: get(data["channel_emoji_status_level_min"]) ?? defaultValue.minChannelWearGiftLevel,
minChannelAutoTranslateLevel: get(data["channel_autotranslation_level_min"]) ?? defaultValue.minChannelAutoTranslateLevel,
minGroupProfileIconLevel: get(data["group_profile_bg_icon_level_min"]) ?? defaultValue.minGroupProfileIconLevel,
minGroupEmojiStatusLevel: get(data["group_emoji_status_level_min"]) ?? defaultValue.minGroupEmojiStatusLevel,
minGroupWallpaperLevel: get(data["group_wallpaper_level_min"]) ?? defaultValue.minGroupWallpaperLevel,
minGroupCustomWallpaperLevel: get(data["group_custom_wallpaper_level_min"]) ?? defaultValue.minGroupCustomWallpaperLevel,
minGroupEmojiPackLevel: get(data["group_emoji_stickers_level_min"]) ?? defaultValue.minGroupEmojiPackLevel,
minGroupAudioTranscriptionLevel: get(data["group_transcribe_level_min"]) ?? defaultValue.minGroupAudioTranscriptionLevel
)
} else {
return defaultValue
}
}
}
public struct AccountFreezeConfiguration {
public static var defaultValue: AccountFreezeConfiguration {
return AccountFreezeConfiguration(
freezeSinceDate: nil,
freezeUntilDate: nil,
freezeAppealUrl: nil
)
}
public let freezeSinceDate: Int32?
public let freezeUntilDate: Int32?
public let freezeAppealUrl: String?
fileprivate init(
freezeSinceDate: Int32?,
freezeUntilDate: Int32?,
freezeAppealUrl: String?
) {
self.freezeSinceDate = freezeSinceDate
self.freezeUntilDate = freezeUntilDate
self.freezeAppealUrl = freezeAppealUrl
}
public static func with(appConfiguration: AppConfiguration) -> AccountFreezeConfiguration {
let defaultValue = self.defaultValue
if let data = appConfiguration.data {
return AccountFreezeConfiguration(
freezeSinceDate: (data["freeze_since_date"] as? Double).flatMap(Int32.init) ?? defaultValue.freezeSinceDate,
freezeUntilDate: (data["freeze_until_date"] as? Double).flatMap(Int32.init) ?? defaultValue.freezeUntilDate,
freezeAppealUrl: data["freeze_appeal_url"] as? String ?? defaultValue.freezeAppealUrl
)
} else {
return defaultValue
}
}
}
public protocol GiftOptionsScreenProtocol {
}
public protocol GiftSetupScreenProtocol {
}
@@ -0,0 +1,627 @@
import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import TelegramAudio
import Display
public enum CallAlreadyInProgressType {
case peer(EnginePeer.Id?)
case external
}
public enum RequestCallResult {
case requested
case alreadyInProgress(CallAlreadyInProgressType)
}
public enum JoinGroupCallManagerResult {
case joined
case alreadyInProgress(CallAlreadyInProgressType)
}
public enum RequestScheduleGroupCallResult {
case success
case alreadyInProgress(CallAlreadyInProgressType)
}
public struct CallAuxiliaryServer {
public enum Connection {
case stun
case turn(username: String, password: String)
}
public let host: String
public let port: Int
public let connection: Connection
public init(
host: String,
port: Int,
connection: Connection
) {
self.host = host
self.port = port
self.connection = connection
}
}
public struct PresentationCallState: Equatable {
public enum State: Equatable {
case waiting
case ringing
case requesting(Bool)
case connecting(Data?)
case active(Double, Int32?, Data)
case reconnecting(Double, Int32?, Data)
case terminating(CallSessionTerminationReason?)
case terminated(CallId?, CallSessionTerminationReason?, Bool)
}
public enum VideoState: Equatable {
case notAvailable
case inactive
case active(isScreencast: Bool, endpointId: String)
case paused(isScreencast: Bool, endpointId: String)
}
public enum RemoteVideoState: Equatable {
case inactive
case active(endpointId: String)
case paused(endpointId: String)
}
public enum RemoteAudioState: Equatable {
case active
case muted
}
public enum RemoteBatteryLevel: Equatable {
case normal
case low
}
public var state: State
public var videoState: VideoState
public var remoteVideoState: RemoteVideoState
public var remoteAudioState: RemoteAudioState
public var remoteBatteryLevel: RemoteBatteryLevel
public var supportsConferenceCalls: Bool
public init(state: State, videoState: VideoState, remoteVideoState: RemoteVideoState, remoteAudioState: RemoteAudioState, remoteBatteryLevel: RemoteBatteryLevel, supportsConferenceCalls: Bool) {
self.state = state
self.videoState = videoState
self.remoteVideoState = remoteVideoState
self.remoteAudioState = remoteAudioState
self.remoteBatteryLevel = remoteBatteryLevel
self.supportsConferenceCalls = supportsConferenceCalls
}
}
public final class PresentationCallVideoView {
public enum Orientation {
case rotation0
case rotation90
case rotation180
case rotation270
}
public let holder: AnyObject
public let view: UIView
public let setOnFirstFrameReceived: (((Float) -> Void)?) -> Void
public let getOrientation: () -> Orientation
public let getAspect: () -> CGFloat
public let setOnOrientationUpdated: (((Orientation, CGFloat) -> Void)?) -> Void
public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void
public let updateIsEnabled: (Bool) -> Void
public init(
holder: AnyObject,
view: UIView,
setOnFirstFrameReceived: @escaping (((Float) -> Void)?) -> Void,
getOrientation: @escaping () -> Orientation,
getAspect: @escaping () -> CGFloat,
setOnOrientationUpdated: @escaping (((Orientation, CGFloat) -> Void)?) -> Void,
setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void,
updateIsEnabled: @escaping (Bool) -> Void
) {
self.holder = holder
self.view = view
self.setOnFirstFrameReceived = setOnFirstFrameReceived
self.getOrientation = getOrientation
self.getAspect = getAspect
self.setOnOrientationUpdated = setOnOrientationUpdated
self.setOnIsMirroredUpdated = setOnIsMirroredUpdated
self.updateIsEnabled = updateIsEnabled
}
}
public enum PresentationCallConferenceState {
case preparing
case ready
}
public protocol PresentationCall: AnyObject {
var context: AccountContext { get }
var isIntegratedWithCallKit: Bool { get }
var internalId: CallSessionInternalId { get }
var peerId: EnginePeer.Id { get }
var isOutgoing: Bool { get }
var isVideo: Bool { get }
var isVideoPossible: Bool { get }
var peer: EnginePeer? { get }
var state: Signal<PresentationCallState, NoError> { get }
var audioLevel: Signal<Float, NoError> { get }
var conferenceState: Signal<PresentationCallConferenceState?, NoError> { get }
var conferenceStateValue: PresentationCallConferenceState? { get }
var conferenceCall: PresentationGroupCall? { get }
var isMuted: Signal<Bool, NoError> { get }
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
var canBeRemoved: Signal<Bool, NoError> { get }
func answer()
func hangUp() -> Signal<Bool, NoError>
func rejectBusy()
func toggleIsMuted()
func setIsMuted(_ value: Bool)
func requestVideo()
func setRequestedVideoAspect(_ aspect: Float)
func disableVideo()
func setOutgoingVideoIsPaused(_ isPaused: Bool)
func switchVideoCamera()
func setCurrentAudioOutput(_ output: AudioSessionOutput)
func debugInfo() -> Signal<(String, String), NoError>
func upgradeToConference(invitePeers: [(id: EnginePeer.Id, isVideo: Bool)], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable
func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
}
public struct VoiceChatConfiguration {
public static var defaultValue: VoiceChatConfiguration {
return VoiceChatConfiguration(videoParticipantsMaxCount: 30)
}
public let videoParticipantsMaxCount: Int32
fileprivate init(videoParticipantsMaxCount: Int32) {
self.videoParticipantsMaxCount = videoParticipantsMaxCount
}
public static func with(appConfiguration: AppConfiguration) -> VoiceChatConfiguration {
if let data = appConfiguration.data, let value = data["groupcall_video_participants_max"] as? Double {
return VoiceChatConfiguration(videoParticipantsMaxCount: Int32(value))
} else {
return .defaultValue
}
}
}
public struct PresentationGroupCallState: Equatable {
public enum NetworkState {
case connecting
case connected
}
public enum DefaultParticipantMuteState {
case unmuted
case muted
}
public enum ConnectionMode {
case rtc
case stream
}
public var myPeerId: EnginePeer.Id
public var networkState: NetworkState
public var connectionMode: ConnectionMode
public var canManageCall: Bool
public var adminIds: Set<EnginePeer.Id>
public var muteState: GroupCallParticipantsContext.Participant.MuteState?
public var defaultParticipantMuteState: DefaultParticipantMuteState?
public var messagesAreEnabled: Bool
public var canEnableMessages: Bool
public var sendPaidMessageStars: Int64?
public var recordingStartTimestamp: Int32?
public var title: String?
public var raisedHand: Bool
public var scheduleTimestamp: Int32?
public var subscribedToScheduled: Bool
public var isVideoEnabled: Bool
public var isVideoWatchersLimitReached: Bool
public var isMyVideoActive: Bool
public var isUnifiedStream: Bool
public var defaultSendAs: EnginePeer.Id?
public init(
myPeerId: EnginePeer.Id,
networkState: NetworkState,
connectionMode: ConnectionMode,
canManageCall: Bool,
adminIds: Set<EnginePeer.Id>,
muteState: GroupCallParticipantsContext.Participant.MuteState?,
defaultParticipantMuteState: DefaultParticipantMuteState?,
messagesAreEnabled: Bool,
canEnableMessages: Bool,
sendPaidMessageStars: Int64?,
recordingStartTimestamp: Int32?,
title: String?,
raisedHand: Bool,
scheduleTimestamp: Int32?,
subscribedToScheduled: Bool,
isVideoEnabled: Bool,
isVideoWatchersLimitReached: Bool,
isMyVideoActive: Bool,
isUnifiedStream: Bool,
defaultSendAs: EnginePeer.Id?
) {
self.myPeerId = myPeerId
self.networkState = networkState
self.connectionMode = connectionMode
self.canManageCall = canManageCall
self.adminIds = adminIds
self.muteState = muteState
self.defaultParticipantMuteState = defaultParticipantMuteState
self.messagesAreEnabled = messagesAreEnabled
self.canEnableMessages = canEnableMessages
self.sendPaidMessageStars = sendPaidMessageStars
self.recordingStartTimestamp = recordingStartTimestamp
self.title = title
self.raisedHand = raisedHand
self.scheduleTimestamp = scheduleTimestamp
self.subscribedToScheduled = subscribedToScheduled
self.isVideoEnabled = isVideoEnabled
self.isVideoWatchersLimitReached = isVideoWatchersLimitReached
self.isMyVideoActive = isMyVideoActive
self.isUnifiedStream = isUnifiedStream
self.defaultSendAs = defaultSendAs
}
}
public struct PresentationGroupCallSummaryState: Equatable {
public var info: GroupCallInfo?
public var participantCount: Int
public var callState: PresentationGroupCallState
public var topParticipants: [GroupCallParticipantsContext.Participant]
public var activeSpeakers: Set<EnginePeer.Id>
public init(
info: GroupCallInfo?,
participantCount: Int,
callState: PresentationGroupCallState,
topParticipants: [GroupCallParticipantsContext.Participant],
activeSpeakers: Set<EnginePeer.Id>
) {
self.info = info
self.participantCount = participantCount
self.callState = callState
self.topParticipants = topParticipants
self.activeSpeakers = activeSpeakers
}
}
public struct PresentationGroupCallMemberState: Equatable {
public var ssrc: UInt32
public var muteState: GroupCallParticipantsContext.Participant.MuteState?
public var speaking: Bool
public init(
ssrc: UInt32,
muteState: GroupCallParticipantsContext.Participant.MuteState?,
speaking: Bool
) {
self.ssrc = ssrc
self.muteState = muteState
self.speaking = speaking
}
}
public enum PresentationGroupCallMuteAction: Equatable {
case muted(isPushToTalkActive: Bool)
case unmuted
public var isEffectivelyMuted: Bool {
switch self {
case let .muted(isPushToTalkActive):
return !isPushToTalkActive
case .unmuted:
return false
}
}
}
public struct PresentationGroupCallMembers: Equatable {
public var participants: [GroupCallParticipantsContext.Participant]
public var speakingParticipants: Set<EnginePeer.Id>
public var totalCount: Int
public var loadMoreToken: String?
public init(
participants: [GroupCallParticipantsContext.Participant],
speakingParticipants: Set<EnginePeer.Id>,
totalCount: Int,
loadMoreToken: String?
) {
self.participants = participants
self.speakingParticipants = speakingParticipants
self.totalCount = totalCount
self.loadMoreToken = loadMoreToken
}
}
public final class PresentationGroupCallMemberEvent {
public let peer: EnginePeer
public let isContact: Bool
public let isInChatList: Bool
public let canUnmute: Bool
public let joined: Bool
public init(peer: EnginePeer, isContact: Bool, isInChatList: Bool, canUnmute: Bool, joined: Bool) {
self.peer = peer
self.isContact = isContact
self.isInChatList = isInChatList
self.canUnmute = canUnmute
self.joined = joined
}
}
public enum PresentationGroupCallTone {
case unmuted
case recordingStarted
}
public struct PresentationGroupCallRequestedVideo: Equatable {
public enum Quality {
case thumbnail
case medium
case full
}
public struct SsrcGroup: Equatable {
public var semantics: String
public var ssrcs: [UInt32]
}
public var audioSsrc: UInt32
public var peerId: Int64
public var endpointId: String
public var ssrcGroups: [SsrcGroup]
public var minQuality: Quality
public var maxQuality: Quality
}
public extension GroupCallParticipantsContext.Participant {
var videoEndpointId: String? {
return self.videoDescription?.endpointId
}
var presentationEndpointId: String? {
return self.presentationDescription?.endpointId
}
}
public extension GroupCallParticipantsContext.Participant {
func requestedVideoChannel(minQuality: PresentationGroupCallRequestedVideo.Quality, maxQuality: PresentationGroupCallRequestedVideo.Quality) -> PresentationGroupCallRequestedVideo? {
guard let audioSsrc = self.ssrc else {
return nil
}
guard let videoDescription = self.videoDescription else {
return nil
}
guard let peer = self.peer else {
return nil
}
return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, peerId: peer.id.id._internalGetInt64Value(), endpointId: videoDescription.endpointId, ssrcGroups: videoDescription.ssrcGroups.map { group in
PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs)
}, minQuality: minQuality, maxQuality: maxQuality)
}
func requestedPresentationVideoChannel(minQuality: PresentationGroupCallRequestedVideo.Quality, maxQuality: PresentationGroupCallRequestedVideo.Quality) -> PresentationGroupCallRequestedVideo? {
guard let audioSsrc = self.ssrc else {
return nil
}
guard let presentationDescription = self.presentationDescription else {
return nil
}
guard let peer = self.peer else {
return nil
}
return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, peerId: peer.id.id._internalGetInt64Value(), endpointId: presentationDescription.endpointId, ssrcGroups: presentationDescription.ssrcGroups.map { group in
PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs)
}, minQuality: minQuality, maxQuality: maxQuality)
}
}
public struct PresentationGroupCallInvitedPeer: Equatable {
public enum State {
case requesting
case ringing
case connecting
}
public var id: EnginePeer.Id
public var state: State?
public init(id: EnginePeer.Id, state: State?) {
self.id = id
self.state = state
}
}
public struct PresentationGroupCallPersistentSettings: Codable {
public static let `default` = PresentationGroupCallPersistentSettings(
isMicrophoneEnabledByDefault: true
)
public var isMicrophoneEnabledByDefault: Bool
public init(isMicrophoneEnabledByDefault: Bool) {
self.isMicrophoneEnabledByDefault = isMicrophoneEnabledByDefault
}
}
public protocol PresentationGroupCall: AnyObject {
var account: Account { get }
var accountContext: AccountContext { get }
var internalId: CallSessionInternalId { get }
var peerId: EnginePeer.Id? { get }
var callId: Int64? { get }
var currentReference: InternalGroupCallReference? { get }
var hasVideo: Bool { get }
var hasScreencast: Bool { get }
var schedulePending: Bool { get }
var isStream: Bool { get }
var isConference: Bool { get }
var conferenceSource: CallSessionInternalId? { get }
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
var isSpeaking: Signal<Bool, NoError> { get }
var canBeRemoved: Signal<Bool, NoError> { get }
var state: Signal<PresentationGroupCallState, NoError> { get }
var stateVersion: Signal<Int, NoError> { get }
var summaryState: Signal<PresentationGroupCallSummaryState?, NoError> { get }
var members: Signal<PresentationGroupCallMembers?, NoError> { get }
var audioLevels: Signal<[(EnginePeer.Id, UInt32, Float, Bool)], NoError> { get }
var myAudioLevel: Signal<Float, NoError> { get }
var myAudioLevelAndSpeaking: Signal<(Float, Bool), NoError> { get }
var isMuted: Signal<Bool, NoError> { get }
var isNoiseSuppressionEnabled: Signal<Bool, NoError> { get }
var e2eEncryptionKeyHash: Signal<Data?, NoError> { get }
var memberEvents: Signal<PresentationGroupCallMemberEvent, NoError> { get }
var reconnectedAsEvents: Signal<EnginePeer, NoError> { get }
var onMutedSpeechActivityDetected: ((Bool) -> Void)? { get set }
func toggleScheduledSubscription(_ subscribe: Bool)
func schedule(timestamp: Int32)
func startScheduled()
func reconnect(with invite: String)
func reconnect(as peerId: EnginePeer.Id)
func leave(terminateIfPossible: Bool) -> Signal<Bool, NoError>
func toggleIsMuted()
func setIsMuted(action: PresentationGroupCallMuteAction)
func setIsNoiseSuppressionEnabled(_ isNoiseSuppressionEnabled: Bool)
func raiseHand()
func lowerHand()
func requestVideo()
func disableVideo()
func disableScreencast()
func switchVideoCamera()
func updateDefaultParticipantsAreMuted(isMuted: Bool)
func updateMessagesEnabled(isEnabled: Bool, sendPaidMessageStars: Int64?)
func setVolume(peerId: EnginePeer.Id, volume: Int32, sync: Bool)
func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo])
func setSuspendVideoChannelRequests(_ value: Bool)
func setCurrentAudioOutput(_ output: AudioSessionOutput)
func playTone(_ tone: PresentationGroupCallTone)
func updateMuteState(peerId: EnginePeer.Id, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState?
func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?, videoOrientation: Bool?)
func updateTitle(_ title: String)
func invitePeer(_ peerId: EnginePeer.Id, isVideo: Bool) -> Bool
func kickPeer(id: EnginePeer.Id)
func removedPeer(_ peerId: EnginePeer.Id)
var invitedPeers: Signal<[PresentationGroupCallInvitedPeer], NoError> { get }
var inviteLinks: Signal<GroupCallInviteLinks?, NoError> { get }
func makeOutgoingVideoView(requestClone: Bool, completion: @escaping (PresentationCallVideoView?, PresentationCallVideoView?) -> Void)
func loadMoreMembers(token: String)
}
public enum VideoChatCall: Equatable {
case group(PresentationGroupCall)
case conferenceSource(PresentationCall)
public static func ==(lhs: VideoChatCall, rhs: VideoChatCall) -> Bool {
switch lhs {
case let .group(lhsGroup):
if case let .group(rhsGroup) = rhs, lhsGroup === rhsGroup {
return true
} else {
return false
}
case let .conferenceSource(lhsConferenceSource):
if case let .conferenceSource(rhsConferenceSource) = rhs, lhsConferenceSource === rhsConferenceSource {
return true
} else {
return false
}
}
}
}
public extension VideoChatCall {
var accountContext: AccountContext {
switch self {
case let .group(group):
return group.accountContext
case let .conferenceSource(conferenceSource):
return conferenceSource.context
}
}
}
public enum PresentationCurrentCall: Equatable {
case call(PresentationCall)
case group(VideoChatCall)
public static func ==(lhs: PresentationCurrentCall, rhs: PresentationCurrentCall) -> Bool {
switch lhs {
case let .call(lhsCall):
if case let .call(rhsCall) = rhs, lhsCall === rhsCall {
return true
} else {
return false
}
case let .group(lhsCall):
if case let .group(rhsCall) = rhs, lhsCall == rhsCall {
return true
} else {
return false
}
}
}
}
public protocol PresentationCallManager: AnyObject {
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
var currentGroupCallSignal: Signal<VideoChatCall?, NoError> { get }
var hasActiveCall: Bool { get }
var hasActiveGroupCall: Bool { get }
func requestCall(context: AccountContext, peerId: EnginePeer.Id, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
func joinGroupCall(context: AccountContext, peerId: EnginePeer.Id, invite: String?, requestJoinAsPeerId: ((@escaping (EnginePeer.Id?) -> Void) -> Void)?, initialCall: EngineGroupCallDescription, endCurrentIfAny: Bool) -> JoinGroupCallManagerResult
func scheduleGroupCall(context: AccountContext, peerId: EnginePeer.Id, endCurrentIfAny: Bool, parentController: ViewController) -> RequestScheduleGroupCallResult
func joinConferenceCall(
accountContext: AccountContext,
initialCall: EngineGroupCallDescription,
reference: InternalGroupCallReference,
beginWithVideo: Bool,
invitePeerIds: [EnginePeer.Id],
endCurrentIfAny: Bool,
unmuteByDefault: Bool
) -> JoinGroupCallManagerResult
}
@@ -0,0 +1,10 @@
import Foundation
import Display
public extension PresentationSurfaceLevel {
static let calls = PresentationSurfaceLevel(rawValue: 1)
static let overlayMedia = PresentationSurfaceLevel(rawValue: 2)
static let notifications = PresentationSurfaceLevel(rawValue: 3)
static let passcode = PresentationSurfaceLevel(rawValue: 4)
static let update = PresentationSurfaceLevel(rawValue: 5)
}
@@ -0,0 +1,80 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AnimationCache
import MultiAnimationRenderer
public enum StorySharingSubject {
case messages([Message])
case gift(StarGift.UniqueGift)
}
public protocol ShareControllerAccountContext: AnyObject {
var accountId: AccountRecordId { get }
var accountPeerId: EnginePeer.Id { get }
var stateManager: AccountStateManager { get }
var engineData: TelegramEngine.EngineData { get }
var animationCache: AnimationCache { get }
var animationRenderer: MultiAnimationRenderer { get }
var contentSettings: ContentSettings { get }
var appConfiguration: AppConfiguration { get }
func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>
}
public protocol ShareControllerEnvironment: AnyObject {
var presentationData: PresentationData { get }
var updatedPresentationData: Signal<PresentationData, NoError> { get }
var isMainApp: Bool { get }
var energyUsageSettings: EnergyUsageSettings { get }
var mediaManager: MediaManager? { get }
func setAccountUserInterfaceInUse(id: AccountRecordId) -> Disposable
func donateSendMessageIntent(account: ShareControllerAccountContext, peerIds: [EnginePeer.Id])
}
public enum ShareControllerExternalStatus {
case preparing(Bool)
case progress(Float)
case done
}
public enum ShareControllerError {
case generic
case fileTooBig(Int64)
}
public enum ShareControllerSubject {
public final class PublicLinkPrefix {
public let visibleString: String
public let actualString: String
public init(visibleString: String, actualString: String) {
self.visibleString = visibleString
self.actualString = actualString
}
}
public final class MediaParameters {
public let startAtTimestamp: Int32?
public let publicLinkPrefix: PublicLinkPrefix?
public init(startAtTimestamp: Int32?, publicLinkPrefix: PublicLinkPrefix?) {
self.startAtTimestamp = startAtTimestamp
self.publicLinkPrefix = publicLinkPrefix
}
}
case url(String)
case text(String)
case quote(text: String, url: String)
case messages([Message])
case image([ImageRepresentationWithReference])
case media(AnyMediaReference, MediaParameters?)
case mapMedia(TelegramMediaMap)
case fromExternal(Int, ([PeerId], [PeerId: Int64], [PeerId: StarsAmount], String, ShareControllerAccountContext, Bool) -> Signal<ShareControllerExternalStatus, ShareControllerError>)
}
@@ -0,0 +1,317 @@
import Foundation
import TelegramCore
import TelegramUIPreferences
import SwiftSignalKit
import UniversalMediaPlayer
import MusicAlbumArtResources
public enum SharedMediaPlaybackDataType {
case music
case voice
case instantVideo
}
public enum SharedMediaPlaybackDataSource: Equatable {
case telegramFile(reference: FileMediaReference, isCopyProtected: Bool, isViewOnce: Bool)
public static func ==(lhs: SharedMediaPlaybackDataSource, rhs: SharedMediaPlaybackDataSource) -> Bool {
switch lhs {
case let .telegramFile(lhsFileReference, lhsIsCopyProtected, lhsIsViewOnce):
if case let .telegramFile(rhsFileReference, rhsIsCopyProtected, rhsIsViewOnce) = rhs {
if !lhsFileReference.media.isEqual(to: rhsFileReference.media) {
return false
}
if lhsIsCopyProtected != rhsIsCopyProtected {
return false
}
if lhsIsViewOnce != rhsIsViewOnce {
return false
}
return true
} else {
return false
}
}
}
}
public struct SharedMediaPlaybackData: Equatable {
public let type: SharedMediaPlaybackDataType
public let source: SharedMediaPlaybackDataSource
public init(type: SharedMediaPlaybackDataType, source: SharedMediaPlaybackDataSource) {
self.type = type
self.source = source
}
public static func ==(lhs: SharedMediaPlaybackData, rhs: SharedMediaPlaybackData) -> Bool {
return lhs.type == rhs.type && lhs.source == rhs.source
}
}
public struct SharedMediaPlaybackAlbumArt: Equatable {
public let thumbnailResource: ExternalMusicAlbumArtResource
public let fullSizeResource: ExternalMusicAlbumArtResource
public init(thumbnailResource: ExternalMusicAlbumArtResource, fullSizeResource: ExternalMusicAlbumArtResource) {
self.thumbnailResource = thumbnailResource
self.fullSizeResource = fullSizeResource
}
}
public enum SharedMediaPlaybackDisplayData: Equatable {
case music(title: String?, performer: String?, albumArt: SharedMediaPlaybackAlbumArt?, long: Bool, caption: NSAttributedString?)
case voice(author: EnginePeer?, peer: EnginePeer?)
case instantVideo(author: EnginePeer?, peer: EnginePeer?, timestamp: Int32)
public static func ==(lhs: SharedMediaPlaybackDisplayData, rhs: SharedMediaPlaybackDisplayData) -> Bool {
switch lhs {
case let .music(lhsTitle, lhsPerformer, lhsAlbumArt, lhsDuration, lhsCaption):
if case let .music(rhsTitle, rhsPerformer, rhsAlbumArt, rhsDuration, rhsCaption) = rhs, lhsTitle == rhsTitle, lhsPerformer == rhsPerformer, lhsAlbumArt == rhsAlbumArt, lhsDuration == rhsDuration, lhsCaption?.string == rhsCaption?.string {
return true
} else {
return false
}
case let .voice(lhsAuthor, lhsPeer):
if case let .voice(rhsAuthor, rhsPeer) = rhs, lhsAuthor == rhsAuthor, lhsPeer == rhsPeer {
return true
} else {
return false
}
case let .instantVideo(lhsAuthor, lhsPeer, lhsTimestamp):
if case let .instantVideo(rhsAuthor, rhsPeer, rhsTimestamp) = rhs, lhsAuthor == rhsAuthor, lhsPeer == rhsPeer, lhsTimestamp == rhsTimestamp {
return true
} else {
return false
}
}
}
}
public protocol SharedMediaPlaylistItem {
var stableId: AnyHashable { get }
var id: SharedMediaPlaylistItemId { get }
var playbackData: SharedMediaPlaybackData? { get }
var displayData: SharedMediaPlaybackDisplayData? { get }
}
public func arePlaylistItemsEqual(_ lhs: SharedMediaPlaylistItem?, _ rhs: SharedMediaPlaylistItem?) -> Bool {
if lhs?.stableId != rhs?.stableId {
return false
}
if lhs?.playbackData != rhs?.playbackData {
return false
}
if lhs?.displayData != rhs?.displayData {
return false
}
return true
}
public protocol SharedMediaPlaylistId {
func isEqual(to: SharedMediaPlaylistId) -> Bool
}
public protocol SharedMediaPlaylistItemId {
func isEqual(to: SharedMediaPlaylistItemId) -> Bool
}
public func areSharedMediaPlaylistItemIdsEqual(_ lhs: SharedMediaPlaylistItemId?, _ rhs: SharedMediaPlaylistItemId?) -> Bool {
if let lhs = lhs, let rhs = rhs {
return lhs.isEqual(to: rhs)
} else if (lhs != nil) != (rhs != nil) {
return false
} else {
return true
}
}
public struct PeerMessagesMediaPlaylistItemId: SharedMediaPlaylistItemId {
public let messageId: EngineMessage.Id
public let messageIndex: EngineMessage.Index
public init(messageId: EngineMessage.Id, messageIndex: EngineMessage.Index) {
self.messageId = messageId
self.messageIndex = messageIndex
}
public func isEqual(to: SharedMediaPlaylistItemId) -> Bool {
if let to = to as? PeerMessagesMediaPlaylistItemId {
if self.messageId != to.messageId || self.messageIndex != to.messageIndex {
return false
}
return true
}
return false
}
}
public protocol SharedMediaPlaylistLocation {
func isEqual(to: SharedMediaPlaylistLocation) -> Bool
}
public func areSharedMediaPlaylistsEqual(_ lhs: SharedMediaPlaylist?, _ rhs: SharedMediaPlaylist?) -> Bool {
if let lhs = lhs, let rhs = rhs {
return lhs.id.isEqual(to: rhs.id) && lhs.location.isEqual(to: rhs.location)
} else if (lhs != nil) != (rhs != nil) {
return false
} else {
return true
}
}
public protocol SharedMediaPlaylist: AnyObject {
var id: SharedMediaPlaylistId { get }
var location: SharedMediaPlaylistLocation { get }
var state: Signal<SharedMediaPlaylistState, NoError> { get }
var looping: MusicPlaybackSettingsLooping { get }
var currentItemDisappeared: (() -> Void)? { get set }
func control(_ action: SharedMediaPlaylistControlAction)
func setOrder(_ order: MusicPlaybackSettingsOrder)
func setLooping(_ looping: MusicPlaybackSettingsLooping)
func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem)
}
public enum SharedMediaPlayerPlaybackControlAction {
case play
case pause
case togglePlayPause
}
public enum SharedMediaPlayerControlAction {
case next
case previous
case playback(SharedMediaPlayerPlaybackControlAction)
case seek(Double)
case setOrder(MusicPlaybackSettingsOrder)
case setLooping(MusicPlaybackSettingsLooping)
case setBaseRate(AudioPlaybackRate)
}
public enum SharedMediaPlaylistControlAction {
case next
case previous
}
public final class SharedMediaPlaylistState: Equatable {
public let loading: Bool
public let playedToEnd: Bool
public let item: SharedMediaPlaylistItem?
public let nextItem: SharedMediaPlaylistItem?
public let previousItem: SharedMediaPlaylistItem?
public let order: MusicPlaybackSettingsOrder
public let looping: MusicPlaybackSettingsLooping
public init(loading: Bool, playedToEnd: Bool, item: SharedMediaPlaylistItem?, nextItem: SharedMediaPlaylistItem?, previousItem: SharedMediaPlaylistItem?, order: MusicPlaybackSettingsOrder, looping: MusicPlaybackSettingsLooping) {
self.loading = loading
self.playedToEnd = playedToEnd
self.item = item
self.nextItem = nextItem
self.previousItem = previousItem
self.order = order
self.looping = looping
}
public static func ==(lhs: SharedMediaPlaylistState, rhs: SharedMediaPlaylistState) -> Bool {
if lhs.loading != rhs.loading {
return false
}
if !arePlaylistItemsEqual(lhs.item, rhs.item) {
return false
}
if !arePlaylistItemsEqual(lhs.nextItem, rhs.nextItem) {
return false
}
if !arePlaylistItemsEqual(lhs.previousItem, rhs.previousItem) {
return false
}
if lhs.order != rhs.order {
return false
}
if lhs.looping != rhs.looping {
return false
}
return true
}
}
public final class SharedMediaPlayerItemPlaybackState: Equatable {
public let playlistId: SharedMediaPlaylistId
public let playlistLocation: SharedMediaPlaylistLocation
public let item: SharedMediaPlaylistItem
public let previousItem: SharedMediaPlaylistItem?
public let nextItem: SharedMediaPlaylistItem?
public let status: MediaPlayerStatus
public let order: MusicPlaybackSettingsOrder
public let looping: MusicPlaybackSettingsLooping
public let playerIndex: Int32
public init(playlistId: SharedMediaPlaylistId, playlistLocation: SharedMediaPlaylistLocation, item: SharedMediaPlaylistItem, previousItem: SharedMediaPlaylistItem?, nextItem: SharedMediaPlaylistItem?, status: MediaPlayerStatus, order: MusicPlaybackSettingsOrder, looping: MusicPlaybackSettingsLooping, playerIndex: Int32) {
self.playlistId = playlistId
self.playlistLocation = playlistLocation
self.item = item
self.previousItem = previousItem
self.nextItem = nextItem
self.status = status
self.order = order
self.looping = looping
self.playerIndex = playerIndex
}
public static func ==(lhs: SharedMediaPlayerItemPlaybackState, rhs: SharedMediaPlayerItemPlaybackState) -> Bool {
if !lhs.playlistId.isEqual(to: rhs.playlistId) {
return false
}
if !arePlaylistItemsEqual(lhs.item, rhs.item) {
return false
}
if !arePlaylistItemsEqual(lhs.previousItem, rhs.previousItem) {
return false
}
if !arePlaylistItemsEqual(lhs.nextItem, rhs.nextItem) {
return false
}
if lhs.status != rhs.status {
return false
}
if lhs.playerIndex != rhs.playerIndex {
return false
}
if lhs.order != rhs.order {
return false
}
if lhs.looping != rhs.looping {
return false
}
return true
}
}
public enum SharedMediaPlayerState: Equatable {
case loading
case item(SharedMediaPlayerItemPlaybackState)
public static func ==(lhs: SharedMediaPlayerState, rhs: SharedMediaPlayerState) -> Bool {
switch lhs {
case .loading:
if case .loading = rhs {
return true
} else {
return false
}
case let .item(item):
if case .item(item) = rhs {
return true
} else {
return false
}
}
}
}
public enum SharedMediaPlayerItemPlaybackStateOrLoading: Equatable {
case state(SharedMediaPlayerItemPlaybackState)
case loading
}
@@ -0,0 +1,7 @@
import Foundation
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
public protocol ThemeUpdateManager: AnyObject {
}
@@ -0,0 +1,468 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import TelegramCore
import Display
import TelegramAudio
import UniversalMediaPlayer
import AVFoundation
import RangeSet
public enum UniversalVideoContentVideoQuality: Equatable {
case auto
case quality(Int)
}
public protocol UniversalVideoContentNode: AnyObject {
var ready: Signal<Void, NoError> { get }
var status: Signal<MediaPlayerStatus, NoError> { get }
var bufferingStatus: Signal<(RangeSet<Int64>, Int64)?, NoError> { get }
var isNativePictureInPictureActive: Signal<Bool, NoError> { get }
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition)
func play()
func pause()
func togglePlayPause()
func setSoundEnabled(_ value: Bool)
func seek(_ timestamp: Double)
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
func setSoundMuted(soundMuted: Bool)
func continueWithOverridingAmbientMode(isAmbient: Bool)
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool)
func setBaseRate(_ baseRate: Double)
func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality)
func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?
func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError>
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int
func removePlaybackCompleted(_ index: Int)
func fetchControl(_ control: UniversalVideoNodeFetchControl)
func notifyPlaybackControlsHidden(_ hidden: Bool)
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool)
func enterNativePictureInPicture() -> Bool
func exitNativePictureInPicture()
func setNativePictureInPictureIsActive(_ value: Bool)
}
public protocol UniversalVideoContent {
var id: AnyHashable { get }
var dimensions: CGSize { get }
var duration: Double { get }
func makeContentNode(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode
func isEqual(to other: UniversalVideoContent) -> Bool
}
public extension UniversalVideoContent {
func isEqual(to other: UniversalVideoContent) -> Bool {
return false
}
}
public protocol UniversalVideoDecoration: AnyObject {
var backgroundNode: ASDisplayNode? { get }
var contentContainerNode: ASDisplayNode { get }
var foregroundNode: ASDisplayNode? { get }
func setStatus(_ status: Signal<MediaPlayerStatus?, NoError>)
func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?)
func updateContentNodeSnapshot(_ snapshot: UIView?)
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition)
func tap()
}
public enum UniversalVideoPriority: Int32, Comparable {
case minimal = 0
case secondaryOverlay = 1
case embedded = 2
case gallery = 3
case overlay = 4
public static func <(lhs: UniversalVideoPriority, rhs: UniversalVideoPriority) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
public enum UniversalVideoNodeFetchControl {
case fetch
case cancel
}
public final class UniversalVideoNode: ASDisplayNode {
private let context: AccountContext
private let postbox: Postbox
private let audioSession: ManagedAudioSession
private let manager: UniversalVideoManager
private let content: UniversalVideoContent
private let priority: UniversalVideoPriority
public let decoration: UniversalVideoDecoration
private let autoplay: Bool
private let snapshotContentWhenGone: Bool
private(set) var contentNode: (UniversalVideoContentNode & ASDisplayNode)?
private var contentNodeId: Int32?
private var playbackCompletedIndex: Int?
private var contentRequestIndex: (AnyHashable, Int32)?
public var playbackCompleted: (() -> Void)?
public private(set) var ownsContentNode: Bool = false
public var ownsContentNodeUpdated: ((Bool) -> Void)?
public var duration: Double {
return self.content.duration
}
private let _status = Promise<MediaPlayerStatus?>()
public var status: Signal<MediaPlayerStatus?, NoError> {
return self._status.get()
}
private let _bufferingStatus = Promise<(RangeSet<Int64>, Int64)?>()
public var bufferingStatus: Signal<(RangeSet<Int64>, Int64)?, NoError> {
return self._bufferingStatus.get()
}
private let _isNativePictureInPictureActive = Promise<Bool>()
public var isNativePictureInPictureActive: Signal<Bool, NoError> {
return self._isNativePictureInPictureActive.get()
}
private let _ready = Promise<Void>()
public var ready: Signal<Void, NoError> {
return self._ready.get()
}
public var canAttachContent: Bool = false {
didSet {
if self.canAttachContent != oldValue {
if self.canAttachContent {
assert(self.contentRequestIndex == nil)
let context = self.context
let content = self.content
let postbox = self.postbox
let audioSession = self.audioSession
self.contentRequestIndex = self.manager.attachUniversalVideoContent(content: self.content, priority: self.priority, create: {
return content.makeContentNode(context: context, postbox: postbox, audioSession: audioSession)
}, update: { [weak self] contentNodeAndFlags in
if let strongSelf = self {
strongSelf.updateContentNode(contentNodeAndFlags)
}
})
} else {
assert(self.contentRequestIndex != nil)
if let (id, index) = self.contentRequestIndex {
self.contentRequestIndex = nil
self.manager.detachUniversalVideoContent(id: id, index: index)
}
}
}
}
}
public var hasAttachedContext: Bool {
return self.contentNode != nil
}
public init(context: AccountContext, postbox: Postbox, audioSession: ManagedAudioSession, manager: UniversalVideoManager, decoration: UniversalVideoDecoration, content: UniversalVideoContent, priority: UniversalVideoPriority, autoplay: Bool = false, snapshotContentWhenGone: Bool = false) {
self.context = context
self.postbox = postbox
self.audioSession = audioSession
self.manager = manager
self.content = content
self.priority = priority
self.decoration = decoration
self.autoplay = autoplay
self.snapshotContentWhenGone = snapshotContentWhenGone
super.init()
self.playbackCompletedIndex = self.manager.addPlaybackCompleted(id: self.content.id, { [weak self] in
self?.playbackCompleted?()
})
self._status.set(self.manager.statusSignal(content: self.content))
self._bufferingStatus.set(self.manager.bufferingStatusSignal(content: self.content))
self._isNativePictureInPictureActive.set(self.manager.isNativePictureInPictureActiveSignal(content: self.content))
self.decoration.setStatus(self.status)
if let backgroundNode = self.decoration.backgroundNode {
self.addSubnode(backgroundNode)
}
self.addSubnode(self.decoration.contentContainerNode)
if let foregroundNode = self.decoration.foregroundNode {
self.addSubnode(foregroundNode)
}
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
deinit {
assert(Queue.mainQueue().isCurrent())
if let playbackCompletedIndex = self.playbackCompletedIndex {
self.manager.removePlaybackCompleted(id: self.content.id, index: playbackCompletedIndex)
}
if let (id, index) = self.contentRequestIndex {
self.contentRequestIndex = nil
self.manager.detachUniversalVideoContent(id: id, index: index)
}
}
private func updateContentNode(_ contentNode: ((UniversalVideoContentNode & ASDisplayNode), Bool)?) {
let previous = self.contentNode
self.contentNode = contentNode?.0
if previous !== contentNode?.0 {
if let previous = previous, contentNode?.0 == nil && self.snapshotContentWhenGone {
if let snapshotView = previous.view.snapshotView(afterScreenUpdates: false) {
self.decoration.updateContentNodeSnapshot(snapshotView)
}
}
if let (contentNode, initiatedCreation) = contentNode {
contentNode.layer.removeAllAnimations()
self._ready.set(contentNode.ready)
if initiatedCreation && self.autoplay {
self.play()
}
}
if contentNode?.0 != nil && self.snapshotContentWhenGone {
self.decoration.updateContentNodeSnapshot(nil)
}
self.decoration.updateContentNode(contentNode?.0)
let ownsContentNode = contentNode?.0 !== nil
if self.ownsContentNode != ownsContentNode {
self.ownsContentNode = ownsContentNode
self.ownsContentNodeUpdated?(ownsContentNode)
}
}
if contentNode == nil {
self._ready.set(.single(Void()))
}
}
public func updateLayout(size: CGSize, actualSize: CGSize? = nil, transition: ContainedViewLayoutTransition) {
self.decoration.updateLayout(size: size, actualSize: actualSize ?? size, transition: transition)
}
public func play() {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.play()
}
})
}
public func pause() {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.pause()
}
})
}
public func togglePlayPause() {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.togglePlayPause()
}
})
}
public func setSoundEnabled(_ value: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setSoundEnabled(value)
}
})
}
public func seek(_ timestamp: Double) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.seek(timestamp)
}
})
}
public func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.playOnceWithSound(playAndRecord: playAndRecord, seek: seek, actionAtEnd: actionAtEnd)
}
})
}
public func setSoundMuted(soundMuted: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setSoundMuted(soundMuted: soundMuted)
}
})
}
public func continueWithOverridingAmbientMode(isAmbient: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.continueWithOverridingAmbientMode(isAmbient: isAmbient)
}
})
}
public func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setContinuePlayingWithoutSoundOnLostAudioSession(value)
}
})
}
public func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setForceAudioToSpeaker(forceAudioToSpeaker)
}
})
}
public func setBaseRate(_ baseRate: Double) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setBaseRate(baseRate)
}
})
}
public func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setVideoQuality(videoQuality)
}
})
}
public func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? {
var result: (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode {
result = contentNode.videoQualityState()
}
})
return result
}
public func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> {
var result: Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError>?
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode {
result = contentNode.videoQualityStateSignal()
}
})
return result ?? .single(nil)
}
public func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.continuePlayingWithoutSound(actionAtEnd: actionAtEnd)
}
})
}
public func fetchControl(_ control: UniversalVideoNodeFetchControl) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.fetchControl(control)
}
})
}
public func notifyPlaybackControlsHidden(_ hidden: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.notifyPlaybackControlsHidden(hidden)
}
})
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.decoration.tap()
}
}
public func getVideoLayer() -> AVSampleBufferDisplayLayer? {
guard let contentNode = self.contentNode else {
return nil
}
func findVideoLayer(layer: CALayer) -> AVSampleBufferDisplayLayer? {
if let layer = layer as? AVSampleBufferDisplayLayer {
return layer
}
if let sublayers = layer.sublayers {
for sublayer in sublayers {
if let result = findVideoLayer(layer: sublayer) {
return result
}
}
}
return nil
}
return findVideoLayer(layer: contentNode.layer)
}
public func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy)
}
})
}
public func enterNativePictureInPicture() -> Bool {
var result = false
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
result = contentNode.enterNativePictureInPicture()
}
})
return result
}
public func exitNativePictureInPicture() {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.exitNativePictureInPicture()
}
})
}
public func setNativePictureInPictureIsActive(_ value: Bool) {
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
if let contentNode = contentNode {
contentNode.setNativePictureInPictureIsActive(value)
}
})
}
}
@@ -0,0 +1,24 @@
import Foundation
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
public enum WallpaperUploadManagerStatus {
case none
case uploading(TelegramWallpaper, Float)
case uploaded(TelegramWallpaper, TelegramWallpaper)
public var wallpaper: TelegramWallpaper? {
switch self {
case let .uploading(wallpaper, _), let .uploaded(wallpaper, _):
return wallpaper
default:
return nil
}
}
}
public protocol WallpaperUploadManager: AnyObject {
func stateSignal() -> Signal<WallpaperUploadManagerStatus, NoError>
func presentationDataUpdated(_ presentationData: PresentationData)
}
+21
View File
@@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AccountUtils",
module_name = "AccountUtils",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,51 @@
import Foundation
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import AccountContext
public let maximumNumberOfAccounts = 3
public let maximumPremiumNumberOfAccounts = 4
public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool = false) -> Signal<((AccountContext, EnginePeer)?, [(AccountContext, EnginePeer, Int32)]), NoError> {
let sharedContext = context.sharedContext
return context.sharedContext.activeAccountContexts
|> mapToSignal { primary, activeAccounts, _ -> Signal<((AccountContext, EnginePeer)?, [(AccountContext, EnginePeer, Int32)]), NoError> in
var accounts: [Signal<(AccountContext, EnginePeer, Int32)?, NoError>] = []
func accountWithPeer(_ context: AccountContext) -> Signal<(AccountContext, EnginePeer, Int32)?, NoError> {
return combineLatest(context.account.postbox.peerView(id: context.account.peerId), renderedTotalUnreadCount(accountManager: sharedContext.accountManager, engine: context.engine))
|> map { view, totalUnreadCount -> (EnginePeer?, Int32) in
return (view.peers[view.peerId].flatMap(EnginePeer.init), totalUnreadCount.0)
}
|> distinctUntilChanged { lhs, rhs in
if lhs.0 != rhs.0 {
return false
}
if lhs.1 != rhs.1 {
return false
}
return true
}
|> map { peer, totalUnreadCount -> (AccountContext, EnginePeer, Int32)? in
if let peer = peer {
return (context, peer, totalUnreadCount)
} else {
return nil
}
}
}
for (_, context, _) in activeAccounts {
accounts.append(accountWithPeer(context))
}
return combineLatest(accounts)
|> map { accounts -> ((AccountContext, EnginePeer)?, [(AccountContext, EnginePeer, Int32)]) in
var primaryRecord: (AccountContext, EnginePeer)?
if let first = accounts.filter({ $0?.0.account.id == primary?.account.id }).first, let (account, peer, _) = first {
primaryRecord = (account, peer)
}
let accountRecords: [(AccountContext, EnginePeer, Int32)] = (includePrimary ? accounts : accounts.filter({ $0?.0.account.id != primary?.account.id })).compactMap({ $0 })
return (primaryRecord, accountRecords)
}
}
}
+24
View File
@@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ActionSheetPeerItem",
module_name = "ActionSheetPeerItem",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/TelegramCore:TelegramCore",
"//submodules/Postbox",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/AvatarNode:AvatarNode",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,199 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import TelegramPresentationData
import AvatarNode
import AccountContext
public class ActionSheetPeerItem: ActionSheetItem {
public let accountPeerId: EnginePeer.Id
public let postbox: Postbox
public let network: Network
public let contentSettings: ContentSettings
public let peer: EnginePeer
public let theme: PresentationTheme
public let title: String
public let isSelected: Bool
public let strings: PresentationStrings
public let action: () -> Void
public convenience init(context: AccountContext, peer: EnginePeer, title: String, isSelected: Bool, strings: PresentationStrings, theme: PresentationTheme, action: @escaping () -> Void) {
self.init(
accountPeerId: context.account.peerId,
postbox: context.account.postbox,
network: context.account.network,
contentSettings: context.currentContentSettings.with { $0 },
peer: peer,
title: title,
isSelected: isSelected,
strings: strings,
theme: theme,
action: action
)
}
public init(
accountPeerId: EnginePeer.Id,
postbox: Postbox,
network: Network,
contentSettings: ContentSettings,
peer: EnginePeer,
title: String,
isSelected: Bool,
strings: PresentationStrings,
theme: PresentationTheme,
action: @escaping () -> Void
) {
self.accountPeerId = accountPeerId
self.postbox = postbox
self.network = network
self.contentSettings = contentSettings
self.peer = peer
self.title = title
self.isSelected = isSelected
self.strings = strings
self.theme = theme
self.action = action
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
let node = ActionSheetPeerItemNode(theme: theme)
node.setItem(self)
return node
}
public func updateNode(_ node: ActionSheetItemNode) {
guard let node = node as? ActionSheetPeerItemNode else {
assertionFailure()
return
}
node.setItem(self)
node.requestLayoutUpdate()
}
}
private let avatarFont = avatarPlaceholderFont(size: 15.0)
public class ActionSheetPeerItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let defaultFont: UIFont
private var item: ActionSheetPeerItem?
private let button: HighlightTrackingButton
private let avatarNode: AvatarNode
private let label: ImmediateTextNode
private let checkNode: ASImageNode
private let accessibilityArea: AccessibilityAreaNode
override public init(theme: ActionSheetControllerTheme) {
self.theme = theme
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
self.button = HighlightTrackingButton()
self.button.isAccessibilityElement = false
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
self.avatarNode.isAccessibilityElement = false
self.label = ImmediateTextNode()
self.label.isUserInteractionEnabled = false
self.label.displaysAsynchronously = false
self.label.maximumNumberOfLines = 1
self.label.isAccessibilityElement = false
self.checkNode = ASImageNode()
self.checkNode.displaysAsynchronously = false
self.checkNode.displayWithoutProcessing = true
self.checkNode.image = generateItemListCheckIcon(color: theme.primaryTextColor)
self.checkNode.isAccessibilityElement = false
self.accessibilityArea = AccessibilityAreaNode()
super.init(theme: theme)
self.view.addSubview(self.button)
self.addSubnode(self.avatarNode)
self.addSubnode(self.label)
self.addSubnode(self.checkNode)
self.addSubnode(self.accessibilityArea)
self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
})
}
}
}
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.accessibilityArea.activate = { [weak self] in
self?.buttonPressed()
return true
}
}
func setItem(_ item: ActionSheetPeerItem) {
self.item = item
let defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
let textColor: UIColor = self.theme.primaryTextColor
self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: textColor)
self.avatarNode.setPeer(accountPeerId: item.accountPeerId, postbox: item.postbox, network: item.network, contentSettings: item.contentSettings, theme: item.theme, peer: item.peer)
self.checkNode.isHidden = !item.isSelected
var accessibilityTraits: UIAccessibilityTraits = [.button]
if item.isSelected {
accessibilityTraits.insert(.selected)
}
self.accessibilityArea.accessibilityTraits = accessibilityTraits
self.accessibilityArea.accessibilityLabel = item.title
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 57.0)
self.button.frame = CGRect(origin: CGPoint(), size: size)
let avatarInset: CGFloat = 42.0
let avatarSize: CGFloat = 32.0
self.avatarNode.frame = CGRect(origin: CGPoint(x: 16.0, y: floor((size.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
let labelSize = self.label.updateLayout(CGSize(width: max(1.0, size.width - avatarInset - 16.0 - 16.0 - 30.0), height: size.height))
self.label.frame = CGRect(origin: CGPoint(x: 16.0 + avatarInset, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
if let image = self.checkNode.image {
self.checkNode.frame = CGRect(origin: CGPoint(x: size.width - image.size.width - 16.0, y: floor((size.height - image.size.height) / 2.0)), size: image.size)
}
self.accessibilityArea.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc private func buttonPressed() {
if let item = self.item {
item.action()
}
}
}
+19
View File
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ActivityIndicator",
module_name = "ActivityIndicator",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,217 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
private func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0, lineWidth: CGFloat = 2.0) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(color.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
let cutoutAngle: CGFloat = CGFloat.pi * 30.0 / 180.0
context.addArc(center: CGPoint(x: size.width / 2.0, y: size.height / 2.0), radius: size.width / 2.0 - lineWidth / 2.0, startAngle: 0.0, endAngle: CGFloat.pi * 2.0 - cutoutAngle, clockwise: false)
context.strokePath()
})
}
private func convertIndicatorColor(_ color: UIColor) -> UIColor {
if color.isEqual(UIColor(rgb: 0x007aff)) || color.isEqual(UIColor(rgb: 0x0088ff)) {
return .gray
} else if color.isEqual(UIColor(rgb: 0x2ea6ff)) {
return .white
} else if color.isEqual(UIColor(rgb: 0x000000)) || color.isEqual(UIColor.black) {
return .gray
} else {
return color
}
}
public enum ActivityIndicatorType: Equatable {
case navigationAccent(UIColor)
case custom(UIColor, CGFloat, CGFloat, Bool)
public static func ==(lhs: ActivityIndicatorType, rhs: ActivityIndicatorType) -> Bool {
switch lhs {
case let .navigationAccent(lhsColor):
if case let .navigationAccent(rhsColor) = rhs, lhsColor.isEqual(rhsColor) {
return true
} else {
return false
}
case let .custom(lhsColor, lhsDiameter, lhsWidth, lhsForceCustom):
if case let .custom(rhsColor, rhsDiameter, rhsWidth, rhsForceCustom) = rhs, lhsColor.isEqual(rhsColor), lhsDiameter == rhsDiameter, lhsWidth == rhsWidth, lhsForceCustom == rhsForceCustom {
return true
} else {
return false
}
}
}
}
public enum ActivityIndicatorSpeed {
case regular
case slow
}
public final class ActivityIndicator: ASDisplayNode {
public var type: ActivityIndicatorType {
didSet {
switch self.type {
case let .navigationAccent(color):
self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color)
case let .custom(color, diameter, lineWidth, _):
self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter, lineWidth: lineWidth)
}
switch self.type {
case let .navigationAccent(color):
self.indicatorView?.color = color
case let .custom(color, _, _, _):
self.indicatorView?.color = convertIndicatorColor(color)
}
}
}
private var currentInHierarchy = false
override public var isHidden: Bool {
didSet {
self.updateAnimation()
}
}
private let speed: ActivityIndicatorSpeed
private let indicatorNode: ASImageNode
private var indicatorView: UIActivityIndicatorView?
public init(type: ActivityIndicatorType, speed: ActivityIndicatorSpeed = .regular) {
self.type = type
self.speed = speed
self.indicatorNode = ASImageNode()
self.indicatorNode.isLayerBacked = true
self.indicatorNode.displaysAsynchronously = false
super.init()
if case let .custom(_, _, _, forceCustom) = self.type, forceCustom {
//self.isLayerBacked = true
}
switch type {
case let .navigationAccent(color):
self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color)
case let .custom(color, diameter, lineWidth, forceCustom):
self.indicatorNode.image = generateIndefiniteActivityIndicatorImage(color: color, diameter: diameter, lineWidth: lineWidth)
if forceCustom {
self.addSubnode(self.indicatorNode)
}
}
}
override public func didLoad() {
super.didLoad()
let indicatorView: UIActivityIndicatorView
switch self.type {
case let .navigationAccent(color):
indicatorView = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.large)
indicatorView.color = color
case let .custom(color, diameter, _, forceCustom):
indicatorView = UIActivityIndicatorView(style: diameter < 15.0 ? UIActivityIndicatorView.Style.medium : UIActivityIndicatorView.Style.large)
indicatorView.color = convertIndicatorColor(color)
if !forceCustom {
self.view.addSubview(indicatorView)
}
}
self.indicatorView = indicatorView
let size = self.bounds.size
if !size.width.isZero {
self.layoutContents(size: size)
}
}
private var isAnimating = false {
didSet {
if self.isAnimating != oldValue {
if self.isAnimating {
self.indicatorView?.startAnimating()
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
switch self.speed {
case .regular:
basicAnimation.duration = 0.5
case .slow:
basicAnimation.duration = 0.7
}
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
basicAnimation.beginTime = 1.0
self.indicatorNode.layer.add(basicAnimation, forKey: "progressRotation")
} else {
self.indicatorView?.stopAnimating()
self.indicatorNode.layer.removeAnimation(forKey: "progressRotation")
}
}
}
}
private func updateAnimation() {
self.isAnimating = !self.isHidden && self.currentInHierarchy
}
override public func willEnterHierarchy() {
super.willEnterHierarchy()
self.currentInHierarchy = true
self.updateAnimation()
}
override public func didExitHierarchy() {
super.didExitHierarchy()
self.currentInHierarchy = false
self.updateAnimation()
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
switch self.type {
case .navigationAccent:
return CGSize(width: 22.0, height: 22.0)
case let .custom(_, diameter, _, _):
return CGSize(width: diameter, height: diameter)
}
}
override public func layout() {
super.layout()
let size = self.bounds.size
self.layoutContents(size: size)
}
private func layoutContents(size: CGSize) {
let indicatorSize: CGSize
let shouldScale: Bool
switch self.type {
case .navigationAccent:
indicatorSize = CGSize(width: 22.0, height: 22.0)
shouldScale = false
case let .custom(_, diameter, _, forceDefault):
indicatorSize = CGSize(width: diameter, height: diameter)
shouldScale = !forceDefault
}
self.indicatorNode.frame = CGRect(origin: CGPoint(x: ((size.width - indicatorSize.width) / 2.0), y: ((size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
if shouldScale, let indicatorView = self.indicatorView {
let intrinsicSize = indicatorView.bounds.size
self.subnodeTransform = CATransform3DMakeScale(min(1.0, indicatorSize.width / intrinsicSize.width), min(1.0, indicatorSize.height / intrinsicSize.height), 1.0)
indicatorView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
}
}
}
+25
View File
@@ -0,0 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AdUI",
module_name = "AdUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/Markdown",
],
visibility = [
"//visibility:public",
],
)
+255
View File
@@ -0,0 +1,255 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import Markdown
public final class AdInfoScreen: ViewController {
private final class Node: ViewControllerTracingNode {
private weak var controller: AdInfoScreen?
private let context: AccountContext
private var presentationData: PresentationData
private let titleNode: ImmediateTextNode
private final class LinkNode: HighlightableButtonNode {
private let backgroundNode: ASImageNode
private let textNode: ImmediateTextNode
private let action: () -> Void
init(text: String, color: UIColor, action: @escaping () -> Void) {
self.action = action
self.backgroundNode = ASImageNode()
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 10.0, color: nil, strokeColor: color, strokeWidth: 1.0, backgroundColor: nil)
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(16.0), textColor: color)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.addTarget(self, action:#selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
self.action()
}
func update(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let size = CGSize(width: width, height: 44.0)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
let textSize = self.textNode.updateLayout(CGSize(width: width - 8.0 * 2.0, height: 44.0))
transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize))
return size.height
}
}
private enum Item {
case text(ImmediateTextNode)
case link(LinkNode)
}
private let items: [Item]
private let scrollNode: ASScrollNode
init(controller: AdInfoScreen, context: AccountContext) {
self.controller = controller
self.context = context
self.presentationData = controller.presentationData
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 1
self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.SponsoredMessageInfoScreen_Title, font: NavigationBar.titleFont, textColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor)
self.scrollNode = ASScrollNode()
self.scrollNode.view.showsVerticalScrollIndicator = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = true
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
var openUrl: ((String) -> Void)?
#if DEBUG && false
let rawText = "First Line\n**Bold Text** [Description](http://google.com) text\n[url]\nabcdee"
#else
let rawText = self.presentationData.strings.SponsoredMessageInfoScreen_MarkdownText
#endif
let defaultUrl = self.presentationData.strings.SponsoredMessageInfo_Url
var items: [Item] = []
var didAddUrl = false
for component in rawText.components(separatedBy: "[url]") {
var itemText = component
if itemText.hasPrefix("\n") {
itemText = String(itemText[itemText.index(itemText.startIndex, offsetBy: 1)...])
}
if itemText.hasSuffix("\n") {
itemText = String(itemText[..<itemText.index(itemText.endIndex, offsetBy: -1)])
}
let textNode = ImmediateTextNode()
textNode.maximumNumberOfLines = 0
textNode.attributedText = parseMarkdownIntoAttributedString(itemText, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor),
linkAttribute: { url in
return ("URL", url)
}
))
items.append(.text(textNode))
textNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
}
textNode.tapAttributeAction = { attributes, _ in
if let value = attributes[NSAttributedString.Key(rawValue: "URL")] as? String {
openUrl?(value)
}
}
textNode.linkHighlightColor = self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2)
if !didAddUrl {
didAddUrl = true
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
openUrl?(defaultUrl)
})))
}
}
if !didAddUrl {
didAddUrl = true
items.append(.link(LinkNode(text: self.presentationData.strings.SponsoredMessageInfo_Url, color: self.presentationData.theme.list.itemAccentColor, action: {
openUrl?(defaultUrl)
})))
}
self.items = items
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.scrollNode)
for item in self.items {
switch item {
case let .text(text):
self.scrollNode.addSubnode(text)
case let .link(link):
self.scrollNode.addSubnode(link)
}
}
openUrl = { [weak self] url in
guard let strongSelf = self else {
return
}
strongSelf.context.sharedContext.applicationBindings.openUrl(url)
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
if self.titleNode.supernode == nil {
self.addSubnode(self.titleNode)
}
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left * 2.0 - 80.0 - 16.0 * 2.0, height: 100.0))
transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: floor((navigationHeight - titleSize.height) / 2.0)), size: titleSize))
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
let sideInset: CGFloat = layout.safeInsets.left + 16.0
let maxWidth: CGFloat = layout.size.width - sideInset * 2.0
var contentHeight: CGFloat = navigationHeight + 16.0
for item in self.items {
switch item {
case let .text(text):
let textSize = text.updateLayout(CGSize(width: maxWidth, height: .greatestFiniteMagnitude))
transition.updateFrameAdditive(node: text, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: textSize))
contentHeight += textSize.height
case let .link(link):
let linkHeight = link.update(width: maxWidth, transition: transition)
let linkSize = CGSize(width: maxWidth, height: linkHeight)
contentHeight += 16.0
transition.updateFrame(node: link, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: linkSize))
contentHeight += linkSize.height
contentHeight += 16.0
}
}
contentHeight += 16.0
contentHeight += layout.intrinsicInsets.bottom
self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: contentHeight)
}
}
private var node: Node {
return self.displayNode as! Node
}
private let context: AccountContext
fileprivate var presentationData: PresentationData
public init(context: AccountContext, forceDark: Bool = false) {
self.context = context
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
if forceDark {
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
}
self.presentationData = presentationData
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.navigationPresentation = .modal
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "", style: .plain, target: self, action: #selector(self.noAction)), animated: false)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: false)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func noAction() {
}
@objc private func donePressed() {
self.dismiss()
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self, context: self.context)
super.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
+21
View File
@@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertUI",
module_name = "AlertUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,355 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TextNodeWithEntities
private let alertWidth: CGFloat = 270.0
final class TextAlertWithEntitiesContentNode: AlertContentNode {
private var theme: AlertControllerTheme
private let actionLayout: TextAlertContentActionLayout
private let titleNode: ImmediateTextNode?
private let textNode: ImmediateTextNodeWithEntities
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
private let _dismissOnOutsideTap: Bool
override public var dismissOnOutsideTap: Bool {
return self._dismissOnOutsideTap
}
private var highlightedItemIndex: Int? = nil
var textAttributeAction: (NSAttributedString.Key, (Any) -> Void)? {
didSet {
if let (attribute, textAttributeAction) = self.textAttributeAction {
self.textNode.highlightAttributeAction = { attributes in
if let _ = attributes[attribute] {
return attribute
} else {
return nil
}
}
self.textNode.tapAttributeAction = { attributes, _ in
if let value = attributes[attribute] {
textAttributeAction(value)
}
}
self.textNode.linkHighlightColor = self.theme.accentColor.withAlphaComponent(0.5)
} else {
self.textNode.highlightAttributeAction = nil
self.textNode.tapAttributeAction = nil
}
}
}
init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout, dismissOnOutsideTap: Bool) {
self.theme = theme
self.actionLayout = actionLayout
self._dismissOnOutsideTap = dismissOnOutsideTap
if let title = title {
let titleNode = ImmediateTextNode()
titleNode.attributedText = title
titleNode.displaysAsynchronously = false
titleNode.isUserInteractionEnabled = false
titleNode.maximumNumberOfLines = 4
titleNode.truncationType = .end
titleNode.isAccessibilityElement = true
titleNode.accessibilityLabel = title.string
self.titleNode = titleNode
} else {
self.titleNode = nil
}
self.textNode = ImmediateTextNodeWithEntities()
self.textNode.maximumNumberOfLines = 0
self.textNode.attributedText = text
self.textNode.displaysAsynchronously = false
self.textNode.isLayerBacked = false
self.textNode.isAccessibilityElement = true
self.textNode.accessibilityLabel = text.string
self.textNode.insets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
if text.length != 0 {
if let paragraphStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle {
self.textNode.textAlignment = paragraphStyle.alignment
}
}
self.textNode.spoilerColor = theme.secondaryColor
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodesSeparator.backgroundColor = theme.separatorColor
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
separatorNode.backgroundColor = theme.separatorColor
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
if let titleNode = self.titleNode {
self.addSubnode(titleNode)
}
self.addSubnode(self.textNode)
self.addSubnode(self.actionNodesSeparator)
var i = 0
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
let index = i
actionNode.highlightedUpdated = { [weak self] highlighted in
if highlighted {
self?.highlightedItemIndex = index
}
}
i += 1
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
}
func setHighlightedItemIndex(_ index: Int?, update: Bool = false) {
self.highlightedItemIndex = index
if update {
var i = 0
for actionNode in self.actionNodes {
if i == index {
actionNode.setHighlighted(true, animated: false)
} else {
actionNode.setHighlighted(false, animated: false)
}
i += 1
}
}
}
override func decreaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? 0
self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true)
}
override func increaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? -1
self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true)
}
override func performHighlightedAction() {
guard let highlightedItemIndex = self.highlightedItemIndex else {
return
}
var i = 0
for itemNode in self.actionNodes {
if i == highlightedItemIndex {
itemNode.performAction()
return
}
i += 1
}
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.theme = theme
if let titleNode = self.titleNode, let attributedText = titleNode.attributedText {
let updatedText = NSMutableAttributedString(attributedString: attributedText)
updatedText.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.primaryColor, range: NSRange(location: 0, length: updatedText.length))
titleNode.attributedText = updatedText
}
if let attributedText = self.textNode.attributedText {
let updatedText = NSMutableAttributedString(attributedString: attributedText)
updatedText.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.primaryColor, range: NSRange(location: 0, length: updatedText.length))
self.textNode.attributedText = updatedText
}
self.textNode.spoilerColor = theme.secondaryColor
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
self.validLayout = size
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var size = size
size.width = min(size.width, alertWidth)
var titleSize: CGSize?
if let titleNode = self.titleNode {
titleSize = titleNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
}
let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = self.actionLayout
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let resultSize: CGSize
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let contentWidth = alertWidth - insets.left - insets.right
if let titleNode = self.titleNode, let titleSize = titleSize {
let spacing: CGFloat = 6.0
let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize)
transition.updateFrame(node: titleNode, frame: titleFrame)
let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame.offsetBy(dx: -1.0, dy: -1.0))
resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom)
} else {
let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: insets.top), size: textSize)
transition.updateFrame(node: self.textNode, frame: textFrame)
resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: textSize.height + actionsHeight + insets.top + insets.bottom)
}
self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
public func textWithEntitiesAlertController(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissAutomatically: Bool = true) -> AlertController {
var dismissImpl: (() -> Void)?
let controller = AlertController(theme: theme, contentNode: TextAlertWithEntitiesContentNode(theme: theme, title: title, text: text, actions: actions.map { action in
return TextAlertAction(type: action.type, title: action.title, action: {
if dismissAutomatically {
dismissImpl?()
}
action.action()
})
}, actionLayout: actionLayout, dismissOnOutsideTap: true), allowInputInset: allowInputInset)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
return controller
}
public func textWithEntitiesAlertController(alertContext: AlertControllerContext, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissAutomatically: Bool = true) -> AlertController {
let theme = alertContext.theme
var dismissImpl: (() -> Void)?
let controller = AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title, text: text, actions: actions.map { action in
return TextAlertAction(type: action.type, title: action.title, action: {
if dismissAutomatically {
dismissImpl?()
}
action.action()
})
}, actionLayout: actionLayout, dismissOnOutsideTap: true), allowInputInset: allowInputInset)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
let presentationDataDisposable = alertContext.themeSignal.start(next: { [weak controller] theme in
controller?.theme = theme
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
return controller
}
@@ -0,0 +1,53 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
public final class AlertControllerContext {
public let theme: AlertControllerTheme
public let themeSignal: Signal<AlertControllerTheme, NoError>
public init(theme: AlertControllerTheme, themeSignal: Signal<AlertControllerTheme, NoError>) {
self.theme = theme
self.themeSignal = themeSignal
}
}
public func textAlertController(alertContext: AlertControllerContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController {
let controller = standardTextAlertController(theme: alertContext.theme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction)
let presentationDataDisposable = alertContext.themeSignal.start(next: { [weak controller] theme in
controller?.theme = theme
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
return controller
}
public func richTextAlertController(alertContext: AlertControllerContext, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissAutomatically: Bool = true) -> AlertController {
let theme = alertContext.theme
var dismissImpl: (() -> Void)?
let controller = AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title, text: text, actions: actions.map { action in
return TextAlertAction(type: action.type, title: action.title, action: {
if dismissAutomatically {
dismissImpl?()
}
action.action()
})
}, actionLayout: actionLayout, dismissOnOutsideTap: true), allowInputInset: allowInputInset)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
let presentationDataDisposable = alertContext.themeSignal.start(next: { [weak controller] theme in
controller?.theme = theme
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
return controller
}
+24
View File
@@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedAvatarSetNode",
module_name = "AnimatedAvatarSetNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/AvatarNode:AvatarNode",
"//submodules/AudioBlob:AudioBlob",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,472 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AvatarNode
import SwiftSignalKit
import TelegramCore
import AccountContext
import AudioBlob
public final class AnimatedAvatarSetContext {
public final class Content {
fileprivate final class Item {
fileprivate enum Key: Hashable {
case peer(EnginePeer.Id)
case placeholder(Int)
}
fileprivate let peer: EnginePeer?
fileprivate let placeholderColor: UIColor
fileprivate init(peer: EnginePeer?, placeholderColor: UIColor) {
self.peer = peer
self.placeholderColor = placeholderColor
}
}
fileprivate var items: [(Item.Key, Item)]
fileprivate init(items: [(Item.Key, Item)]) {
self.items = items
}
}
private final class ItemState {
let peer: EnginePeer
init(peer: EnginePeer) {
self.peer = peer
}
}
private var peers: [EnginePeer] = []
private var itemStates: [EnginePeer.Id: ItemState] = [:]
public init() {
}
public func update(peers: [EnginePeer], animated: Bool) -> Content {
var items: [(Content.Item.Key, Content.Item)] = []
for peer in peers {
items.append((.peer(peer.id), Content.Item(peer: peer, placeholderColor: .white)))
}
return Content(items: items)
}
public func updatePlaceholder(color: UIColor, count: Int, animated: Bool) -> Content {
var items: [(Content.Item.Key, Content.Item)] = []
for i in 0 ..< count {
items.append((.placeholder(i), Content.Item(peer: nil, placeholderColor: color)))
}
return Content(items: items)
}
}
private let sharedAvatarFont: UIFont = avatarPlaceholderFont(size: 12.0)
private final class ContentNode: ASDisplayNode {
private let context: AccountContext
private var audioLevelView: VoiceBlobView?
private var audioLevelBlobOverlay: UIImageView?
private let unclippedNode: ASImageNode
private let clippedNode: ASImageNode
private var size: CGSize
private var spacing: CGFloat
private var disposable: Disposable?
init(context: AccountContext, peer: EnginePeer?, placeholderColor: UIColor, font: UIFont, synchronousLoad: Bool, size: CGSize, spacing: CGFloat) {
self.context = context
self.size = size
self.spacing = spacing
self.unclippedNode = ASImageNode()
self.clippedNode = ASImageNode()
super.init()
self.addSubnode(self.unclippedNode)
self.addSubnode(self.clippedNode)
if let peer = peer {
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) {
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.lightGray.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})!
self.updateImage(image: image, size: size, spacing: spacing)
let disposable = (signal
|> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in
guard let strongSelf = self else {
return
}
let image = imageVersions?.0
if let image = image {
strongSelf.updateImage(image: image, size: size, spacing: spacing)
}
})
self.disposable = disposable
} else {
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
drawPeerAvatarLetters(context: context, size: size, font: font, letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor)
})!
self.updateImage(image: image, size: size, spacing: spacing)
}
} else {
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(placeholderColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})!
self.updateImage(image: image, size: size, spacing: spacing)
}
}
private func updateImage(image: UIImage, size: CGSize, spacing: CGFloat) {
self.unclippedNode.image = image
self.clippedNode.image = generateImage(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)
context.draw(image.cgImage!, in: 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)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.5, dy: -1.5).offsetBy(dx: spacing - size.width, dy: 0.0))
})
}
deinit {
self.disposable?.dispose()
}
func updateLayout(size: CGSize, isClipped: Bool, animated: Bool) {
self.unclippedNode.frame = CGRect(origin: CGPoint(), size: size)
self.clippedNode.frame = CGRect(origin: CGPoint(), size: size)
if animated && self.unclippedNode.alpha.isZero != self.clippedNode.alpha.isZero {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.unclippedNode, alpha: isClipped ? 0.0 : 1.0)
transition.updateAlpha(node: self.clippedNode, alpha: isClipped ? 1.0 : 0.0)
} else {
self.unclippedNode.alpha = isClipped ? 0.0 : 1.0
self.clippedNode.alpha = isClipped ? 1.0 : 0.0
}
}
func updateAudioLevel(color: UIColor, backgroundColor: UIColor, value: Float) {
if self.audioLevelView == nil, value > 0.0, self.context.sharedContext.energyUsageSettings.fullTranslucency {
let blobFrame = self.unclippedNode.bounds.insetBy(dx: -8.0, dy: -8.0)
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 0.3,
smallBlobRange: (0, 0),
mediumBlobRange: (0.7, 0.8),
bigBlobRange: (0.8, 0.9)
)
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
let playbackMaskLayer = CAShapeLayer()
playbackMaskLayer.frame = maskRect
playbackMaskLayer.fillRule = .evenOdd
let maskPath = UIBezierPath()
maskPath.append(UIBezierPath(roundedRect: self.unclippedNode.bounds.offsetBy(dx: 8, dy: 8), cornerRadius: maskRect.width / 2.0))
maskPath.append(UIBezierPath(rect: maskRect))
playbackMaskLayer.path = maskPath.cgPath
//audioLevelView.layer.mask = playbackMaskLayer
audioLevelView.setColor(color)
self.audioLevelView = audioLevelView
self.view.insertSubview(audioLevelView, at: 0)
}
let level = min(1.0, max(0.0, CGFloat(value)))
if let audioLevelView = self.audioLevelView {
audioLevelView.updateLevel(CGFloat(value) * 2.0)
let avatarScale: CGFloat
let audioLevelScale: CGFloat
if value > 0.0 {
audioLevelView.startAnimating()
avatarScale = 1.03 + level * 0.07
audioLevelScale = 1.0
} else {
audioLevelView.stopAnimating(duration: 0.5)
avatarScale = 1.0
audioLevelScale = 0.01
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateSublayerTransformScale(node: self, scale: CGPoint(x: avatarScale, y: avatarScale), beginWithCurrentState: true)
transition.updateSublayerTransformScale(layer: audioLevelView.layer, scale: CGPoint(x: audioLevelScale, y: audioLevelScale), beginWithCurrentState: true)
}
}
}
public final class AnimatedAvatarSetNode: ASDisplayNode {
private var contentNodes: [AnimatedAvatarSetContext.Content.Item.Key: ContentNode] = [:]
override public init() {
super.init()
}
public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), customSpacing: CGFloat? = nil, font: UIFont? = nil, animated: Bool, synchronousLoad: Bool) -> CGSize {
var contentWidth: CGFloat = 0.0
let contentHeight: CGFloat = itemSize.height
let spacing: CGFloat
if let customSpacing = customSpacing {
spacing = customSpacing
} else {
spacing = 10.0
}
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
var index = 0
for i in 0 ..< content.items.count {
let (key, item) = content.items[i]
validKeys.append(key)
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
let itemNode: ContentNode
if let current = self.contentNodes[key] {
itemNode = current
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: animated)
transition.updateFrame(node: itemNode, frame: itemFrame)
} else {
itemNode = ContentNode(context: context, peer: item.peer, placeholderColor: item.placeholderColor, font: font ?? sharedAvatarFont, synchronousLoad: synchronousLoad, size: itemSize, spacing: spacing)
self.addSubnode(itemNode)
self.contentNodes[key] = itemNode
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
itemNode.frame = itemFrame
if animated {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
}
}
itemNode.zPosition = CGFloat(100 - i)
if i == content.items.count - 1 {
contentWidth += itemSize.width
} else {
contentWidth += itemSize.width - spacing
}
index += 1
}
var removeKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
for key in self.contentNodes.keys {
if !validKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
guard let itemNode = self.contentNodes.removeValue(forKey: key) else {
continue
}
if animated {
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
} else {
itemNode.removeFromSupernode()
}
}
return CGSize(width: contentWidth, height: contentHeight)
}
public func updateAudioLevels(color: UIColor, backgroundColor: UIColor, levels: [EnginePeer.Id: Float]) {
for (key, itemNode) in self.contentNodes {
if case let .peer(peerId) = key, let value = levels[peerId] {
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: value)
} else {
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: 0.0)
}
}
}
}
public final class AnimatedAvatarSetView: UIView {
private final class ContentView: UIView {
private let unclippedView: UIImageView
private let clippedView: UIImageView
private var size: CGSize
private var spacing: CGFloat
private var disposable: Disposable?
init(context: AccountContext, peer: EnginePeer?, placeholderColor: UIColor, font: UIFont, synchronousLoad: Bool, size: CGSize, spacing: CGFloat) {
self.size = size
self.spacing = spacing
self.unclippedView = UIImageView()
self.clippedView = UIImageView()
super.init(frame: CGRect())
self.addSubview(self.unclippedView)
self.addSubview(self.clippedView)
if let peer = peer {
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) {
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.lightGray.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})!
self.updateImage(image: image, size: size, spacing: spacing)
let disposable = (signal
|> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in
guard let strongSelf = self else {
return
}
let image = imageVersions?.0
if let image = image {
strongSelf.updateImage(image: image, size: size, spacing: spacing)
}
})
self.disposable = disposable
} else {
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
drawPeerAvatarLetters(context: context, size: size, font: font, letters: peer.displayLetters, peerId: peer.id, nameColor: peer.nameColor)
})!
self.updateImage(image: image, size: size, spacing: spacing)
}
} else {
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(placeholderColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})!
self.updateImage(image: image, size: size, spacing: spacing)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateImage(image: UIImage, size: CGSize, spacing: CGFloat) {
self.unclippedView.image = image
self.clippedView.image = generateImage(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)
context.draw(image.cgImage!, in: 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)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.5, dy: -1.5).offsetBy(dx: spacing - size.width, dy: 0.0))
})
}
deinit {
self.disposable?.dispose()
}
func updateLayout(size: CGSize, isClipped: Bool, animated: Bool) {
self.unclippedView.frame = CGRect(origin: CGPoint(), size: size)
self.clippedView.frame = CGRect(origin: CGPoint(), size: size)
if animated && self.unclippedView.alpha.isZero != self.clippedView.alpha.isZero {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(layer: self.unclippedView.layer, alpha: isClipped ? 0.0 : 1.0)
transition.updateAlpha(layer: self.clippedView.layer, alpha: isClipped ? 1.0 : 0.0)
} else {
self.unclippedView.alpha = isClipped ? 0.0 : 1.0
self.clippedView.alpha = isClipped ? 1.0 : 0.0
}
}
}
private var contentViews: [AnimatedAvatarSetContext.Content.Item.Key: ContentView] = [:]
public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), customSpacing: CGFloat? = nil, font: UIFont? = nil, animation: ListViewItemUpdateAnimation, synchronousLoad: Bool) -> CGSize {
var contentWidth: CGFloat = 0.0
let contentHeight: CGFloat = itemSize.height
let spacing: CGFloat
if let customSpacing = customSpacing {
spacing = customSpacing
} else {
spacing = 10.0
}
var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
var index = 0
for i in 0 ..< content.items.count {
let (key, item) = content.items[i]
validKeys.append(key)
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
let itemView: ContentView
if let current = self.contentViews[key] {
itemView = current
itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: animation.isAnimated)
animation.animator.updateFrame(layer: itemView.layer, frame: itemFrame, completion: nil)
} else {
itemView = ContentView(context: context, peer: item.peer, placeholderColor: item.placeholderColor, font: font ?? sharedAvatarFont, synchronousLoad: synchronousLoad, size: itemSize, spacing: spacing)
self.addSubview(itemView)
self.contentViews[key] = itemView
itemView.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
itemView.frame = itemFrame
if animation.isAnimated {
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
itemView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
}
}
itemView.layer.zPosition = CGFloat(100 - i)
contentWidth += itemSize.width - spacing
index += 1
}
var removeKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
for key in self.contentViews.keys {
if !validKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
guard let itemView = self.contentViews.removeValue(forKey: key) else {
continue
}
if animation.isAnimated {
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemView] _ in
itemView?.removeFromSuperview()
})
itemView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
} else {
itemView.removeFromSuperview()
}
}
return CGSize(width: contentWidth, height: contentHeight)
}
}
+19
View File
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedCountLabelNode",
module_name = "AnimatedCountLabelNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,548 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
public class AnimatedCountLabelNode: ASDisplayNode {
public struct Layout {
public var size: CGSize
public var isTruncated: Bool
}
public enum Segment: Equatable {
case number(Int, NSAttributedString)
case text(Int, NSAttributedString)
public static func ==(lhs: Segment, rhs: Segment) -> Bool {
switch lhs {
case let .number(number, text):
if case let .number(rhsNumber, rhsText) = rhs, number == rhsNumber, text.isEqual(to: rhsText) {
return true
} else {
return false
}
case let .text(index, text):
if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) {
return true
} else {
return false
}
}
}
}
fileprivate enum ResolvedSegment: Equatable {
public enum Key: Hashable {
case number(Int)
case text(Int)
}
case number(id: Int, value: Int, string: NSAttributedString)
case text(id: Int, string: NSAttributedString)
public static func ==(lhs: ResolvedSegment, rhs: ResolvedSegment) -> Bool {
switch lhs {
case let .number(id, number, text):
if case let .number(rhsId, rhsNumber, rhsText) = rhs, id == rhsId, number == rhsNumber, text.isEqual(to: rhsText) {
return true
} else {
return false
}
case let .text(index, text):
if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) {
return true
} else {
return false
}
}
}
public var attributedText: NSAttributedString {
switch self {
case let .number(_, _, text):
return text
case let .text(_, text):
return text
}
}
var key: Key {
switch self {
case let .number(id, _, _):
return .number(id)
case let .text(index, _):
return .text(index)
}
}
}
fileprivate var resolvedSegments: [ResolvedSegment.Key: (ResolvedSegment, TextNode)] = [:]
public var reverseAnimationDirection: Bool = false
public var alwaysOneDirection: Bool = false
override public init() {
super.init()
}
public func asyncLayout() -> (CGSize, UIEdgeInsets, [Segment]) -> (Layout, (Bool) -> Void) {
var segmentLayouts: [ResolvedSegment.Key: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = [:]
let wasEmpty = self.resolvedSegments.isEmpty
for (segmentKey, segmentAndTextNode) in self.resolvedSegments {
segmentLayouts[segmentKey] = TextNode.asyncLayout(segmentAndTextNode.1)
}
let reverseAnimationDirection = self.reverseAnimationDirection
let alwaysOneDirection = self.alwaysOneDirection
return { [weak self] size, insets, initialSegments in
var segments: [ResolvedSegment] = []
loop: for segment in initialSegments {
switch segment {
case let .number(value, string):
if string.string.isEmpty {
continue loop
}
let attributes = string.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: 1))
var remainingValue = value
let insertPosition = segments.count
while true {
let digitValue = remainingValue % 10
segments.insert(.number(id: 1000 - segments.count, value: value, string: NSAttributedString(string: "\(digitValue)", attributes: attributes)), at: insertPosition)
remainingValue /= 10
if remainingValue == 0 {
break
}
}
case let .text(id, string):
segments.append(.text(id: id, string: string))
}
}
for segment in segments {
if segmentLayouts[segment.key] == nil {
segmentLayouts[segment.key] = TextNode.asyncLayout(nil)
}
}
var contentSize = CGSize()
var remainingSize = size
var calculatedSegments: [ResolvedSegment.Key: (TextNodeLayout, CGFloat, () -> TextNode)] = [:]
var isTruncated = false
var validKeys: [ResolvedSegment.Key] = []
for segment in segments {
validKeys.append(segment.key)
let (layout, apply) = segmentLayouts[segment.key]!(TextNodeLayoutArguments(attributedString: segment.attributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: remainingSize, alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil))
var effectiveSegmentWidth = layout.size.width
if case .number = segment {
//effectiveSegmentWidth = ceil(effectiveSegmentWidth / 2.0) * 2.0
} else if segment.attributedText.string == " " {
effectiveSegmentWidth = max(effectiveSegmentWidth, 4.0)
}
calculatedSegments[segment.key] = (layout, effectiveSegmentWidth, apply)
contentSize.width += effectiveSegmentWidth
contentSize.height = max(contentSize.height, layout.size.height)
remainingSize.width = max(0.0, remainingSize.width - layout.size.width)
if layout.truncated {
isTruncated = true
}
}
return (Layout(size: contentSize, isTruncated: isTruncated), { animated in
guard let strongSelf = self else {
return
}
let transition: ContainedViewLayoutTransition
if animated && !wasEmpty {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
var currentOffset = CGPoint(x: insets.left, y: 0.0)
for segment in segments {
var animation: (CGFloat, Double)?
if let (currentSegment, currentTextNode) = strongSelf.resolvedSegments[segment.key] {
if case let .number(_, currentValue, currentString) = currentSegment, case let .number(_, updatedValue, updatedString) = segment, animated, !wasEmpty, currentValue != updatedValue, currentString.string != updatedString.string, let snapshot = currentTextNode.layer.snapshotContentTree() {
var fromAlpha: CGFloat = 1.0
if let presentation = currentTextNode.layer.presentation() {
fromAlpha = CGFloat(presentation.opacity)
}
var offsetY: CGFloat
if currentValue > updatedValue || alwaysOneDirection {
offsetY = -floor(currentTextNode.bounds.height * 0.6)
} else {
offsetY = floor(currentTextNode.bounds.height * 0.6)
}
if reverseAnimationDirection {
offsetY = -offsetY
}
animation = (-offsetY, 0.2)
snapshot.frame = currentTextNode.frame
strongSelf.layer.addSublayer(snapshot)
snapshot.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offsetY), duration: 0.2, removeOnCompletion: false, additive: true)
snapshot.animateScale(from: 1.0, to: 0.3, duration: 0.2, removeOnCompletion: false)
snapshot.animateAlpha(from: fromAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshot] _ in
snapshot?.removeFromSuperlayer()
})
}
}
let (layout, effectiveSegmentWidth, apply) = calculatedSegments[segment.key]!
let textNode = apply()
let textFrame = CGRect(origin: currentOffset, size: layout.size)
if textNode.frame.isEmpty {
textNode.frame = textFrame
if animated, !wasEmpty, animation == nil {
textNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else if textNode.frame != textFrame {
transition.updateFrameAdditive(node: textNode, frame: textFrame)
}
currentOffset.x += effectiveSegmentWidth
if let (_, currentTextNode) = strongSelf.resolvedSegments[segment.key] {
if currentTextNode !== textNode {
currentTextNode.removeFromSupernode()
strongSelf.addSubnode(textNode)
}
} else {
strongSelf.addSubnode(textNode)
textNode.displaysAsynchronously = false
textNode.isUserInteractionEnabled = false
}
if let (offset, duration) = animation {
textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, additive: true)
textNode.layer.animateScale(from: 0.3, to: 1.0, duration: duration)
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
strongSelf.resolvedSegments[segment.key] = (segment, textNode)
}
var removeKeys: [ResolvedSegment.Key] = []
for key in strongSelf.resolvedSegments.keys {
if !validKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
guard let (_, textNode) = strongSelf.resolvedSegments.removeValue(forKey: key) else {
continue
}
if animated {
textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textNode] _ in
textNode?.removeFromSupernode()
})
} else {
textNode.removeFromSupernode()
}
}
})
}
}
}
public final class ImmediateAnimatedCountLabelNode: AnimatedCountLabelNode {
public var segments: [AnimatedCountLabelNode.Segment] = []
private var constrainedSize: CGSize?
private var insets: UIEdgeInsets?
public func updateLayout(size: CGSize, insets: UIEdgeInsets = .zero, animated: Bool) -> CGSize {
self.constrainedSize = size
self.insets = insets
let makeLayout = self.asyncLayout()
let (layout, apply) = makeLayout(size, insets, self.segments)
let _ = apply(animated)
return layout.size
}
public func makeCopy() -> ASDisplayNode {
let node = ImmediateAnimatedCountLabelNode()
node.frame = self.frame
node.segments = self.segments
if let subnodes = self.subnodes {
for subnode in subnodes {
if let subnode = subnode as? ASImageNode {
let copySubnode = ASImageNode()
copySubnode.isLayerBacked = subnode.isLayerBacked
copySubnode.image = subnode.image
copySubnode.displaysAsynchronously = false
copySubnode.displayWithoutProcessing = true
copySubnode.frame = subnode.frame
copySubnode.alpha = subnode.alpha
node.addSubnode(copySubnode)
}
}
}
if let constrainedSize = self.constrainedSize, let insets = self.insets {
let _ = node.updateLayout(size: constrainedSize, insets: insets, animated: false)
}
return node
}
}
public class AnimatedCountLabelView: UIView {
public struct Layout {
public var size: CGSize
public var isTruncated: Bool
}
public enum Segment: Equatable {
case number(Int, NSAttributedString)
case text(Int, NSAttributedString)
public static func ==(lhs: Segment, rhs: Segment) -> Bool {
switch lhs {
case let .number(number, text):
if case let .number(rhsNumber, rhsText) = rhs, number == rhsNumber, text.isEqual(to: rhsText) {
return true
} else {
return false
}
case let .text(index, text):
if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) {
return true
} else {
return false
}
}
}
}
fileprivate enum ResolvedSegment: Equatable {
public enum Key: Hashable {
case number(Int)
case text(Int)
}
case number(id: Int, value: Int, string: NSAttributedString)
case text(id: Int, string: NSAttributedString)
public static func ==(lhs: ResolvedSegment, rhs: ResolvedSegment) -> Bool {
switch lhs {
case let .number(id, number, text):
if case let .number(rhsId, rhsNumber, rhsText) = rhs, id == rhsId, number == rhsNumber, text.isEqual(to: rhsText) {
return true
} else {
return false
}
case let .text(index, text):
if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) {
return true
} else {
return false
}
}
}
public var attributedText: NSAttributedString {
switch self {
case let .number(_, _, text):
return text
case let .text(_, text):
return text
}
}
var key: Key {
switch self {
case let .number(id, _, _):
return .number(id)
case let .text(index, _):
return .text(index)
}
}
}
fileprivate var resolvedSegments: [ResolvedSegment.Key: (ResolvedSegment, TextNode)] = [:]
public var reverseAnimationDirection: Bool = false
public var alwaysOneDirection: Bool = false
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(size: CGSize, segments initialSegments: [Segment], reducedLetterSpacing: Bool = false, transition: ContainedViewLayoutTransition) -> Layout {
var segmentLayouts: [ResolvedSegment.Key: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = [:]
let wasEmpty = self.resolvedSegments.isEmpty
for (segmentKey, segmentAndTextNode) in self.resolvedSegments {
segmentLayouts[segmentKey] = TextNode.asyncLayout(segmentAndTextNode.1)
}
let reverseAnimationDirection = self.reverseAnimationDirection
let alwaysOneDirection = self.alwaysOneDirection
var segments: [ResolvedSegment] = []
loop: for segment in initialSegments {
switch segment {
case let .number(value, string):
if string.string.isEmpty {
continue loop
}
let attributes = string.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: 1))
for character in string.string {
if let _ = Int(String(character)) {
segments.append(.number(id: 1000 + segments.count, value: value, string: NSAttributedString(string: String(character), attributes: attributes)))
} else {
segments.append(.text(id: 1000 + segments.count, string: NSAttributedString(string: String(character), attributes: attributes)))
}
}
/*var remainingValue = value
let insertPosition = segments.count
while true {
let digitValue = remainingValue % 10
segments.insert(.number(id: 1000 - segments.count, value: value, string: NSAttributedString(string: "\(digitValue)", attributes: attributes)), at: insertPosition)
remainingValue /= 10
if remainingValue == 0 {
break
}
}*/
case let .text(id, string):
segments.append(.text(id: id, string: string))
}
}
for segment in segments {
if segmentLayouts[segment.key] == nil {
segmentLayouts[segment.key] = TextNode.asyncLayout(nil)
}
}
var contentSize = CGSize()
var remainingSize = size
var calculatedSegments: [ResolvedSegment.Key: (TextNodeLayout, CGFloat, () -> TextNode)] = [:]
var isTruncated = false
var validKeys: [ResolvedSegment.Key] = []
for segment in segments {
validKeys.append(segment.key)
let (layout, apply) = segmentLayouts[segment.key]!(TextNodeLayoutArguments(attributedString: segment.attributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: remainingSize, alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil))
var effectiveSegmentWidth = layout.size.width
if case .number = segment {
//effectiveSegmentWidth = ceil(effectiveSegmentWidth / 2.0) * 2.0
} else if segment.attributedText.string == " " {
effectiveSegmentWidth = max(effectiveSegmentWidth, 4.0)
}
calculatedSegments[segment.key] = (layout, effectiveSegmentWidth, apply)
contentSize.width += floor(effectiveSegmentWidth * 0.9)
contentSize.height = max(contentSize.height, layout.size.height)
remainingSize.width = max(0.0, remainingSize.width - layout.size.width)
if layout.truncated {
isTruncated = true
}
}
var transition = transition
if wasEmpty {
transition = .immediate
}
var currentOffset = CGPoint()
for segment in segments {
var animation: (CGFloat, Double)?
if let (currentSegment, currentTextNode) = self.resolvedSegments[segment.key] {
if case let .number(_, currentValue, currentString) = currentSegment, case let .number(_, updatedValue, updatedString) = segment, transition.isAnimated, !wasEmpty, currentValue != updatedValue, currentString.string != updatedString.string, let snapshot = currentTextNode.layer.snapshotContentTree() {
var fromAlpha: CGFloat = 1.0
if let presentation = currentTextNode.layer.presentation() {
fromAlpha = CGFloat(presentation.opacity)
}
var offsetY: CGFloat
if currentValue < updatedValue || alwaysOneDirection {
offsetY = -floor(currentTextNode.bounds.height * 0.6)
} else {
offsetY = floor(currentTextNode.bounds.height * 0.6)
}
if reverseAnimationDirection {
offsetY = -offsetY
}
animation = (-offsetY, 0.2)
snapshot.frame = currentTextNode.frame
self.layer.addSublayer(snapshot)
snapshot.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offsetY), duration: 0.2, removeOnCompletion: false, additive: true)
snapshot.animateScale(from: 1.0, to: 0.3, duration: 0.2, removeOnCompletion: false)
snapshot.animateAlpha(from: fromAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshot] _ in
snapshot?.removeFromSuperlayer()
})
}
}
let (layout, effectiveSegmentWidth, apply) = calculatedSegments[segment.key]!
let textNode = apply()
let textFrame = CGRect(origin: currentOffset, size: layout.size)
if textNode.frame.isEmpty {
textNode.frame = textFrame
if transition.isAnimated, !wasEmpty, animation == nil {
textNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else if textNode.frame != textFrame {
transition.updateFrameAdditive(node: textNode, frame: textFrame)
}
if reducedLetterSpacing {
currentOffset.x += effectiveSegmentWidth * 0.9
} else {
currentOffset.x += effectiveSegmentWidth
}
if let (_, currentTextNode) = self.resolvedSegments[segment.key] {
if currentTextNode !== textNode {
currentTextNode.removeFromSupernode()
self.addSubnode(textNode)
}
} else {
textNode.displaysAsynchronously = false
textNode.isUserInteractionEnabled = false
self.addSubview(textNode.view)
}
if let (offset, duration) = animation {
textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, additive: true)
textNode.layer.animateScale(from: 0.3, to: 1.0, duration: duration)
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
self.resolvedSegments[segment.key] = (segment, textNode)
}
var removeKeys: [ResolvedSegment.Key] = []
for key in self.resolvedSegments.keys {
if !validKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
guard let (_, textNode) = self.resolvedSegments.removeValue(forKey: key) else {
continue
}
if transition.isAnimated {
textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textNode] _ in
textNode?.removeFromSupernode()
})
} else {
textNode.removeFromSupernode()
}
}
return Layout(size: contentSize, isTruncated: isTruncated)
}
}
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedNavigationStripeNode",
module_name = "AnimatedNavigationStripeNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,310 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
public final class AnimatedNavigationStripeNode: ASDisplayNode {
public struct Colors: Equatable {
public var foreground: UIColor
public var background: UIColor
public var clearBackground: UIColor
public init(
foreground: UIColor,
background: UIColor,
clearBackground: UIColor
) {
self.foreground = foreground
self.background = background
self.clearBackground = clearBackground
}
public static func ==(lhs: Colors, rhs: Colors) -> Bool {
if !lhs.foreground.isEqual(rhs.foreground) {
return false
}
if !lhs.background.isEqual(rhs.background) {
return false
}
if !lhs.clearBackground.isEqual(rhs.clearBackground) {
return false
}
return true
}
}
public struct Configuration: Equatable {
public var height: CGFloat
public var index: Int
public var count: Int
public init(height: CGFloat, index: Int, count: Int) {
self.height = height
self.index = index
self.count = count
}
}
private final class BackgroundLineNode {
let lineNode: ASImageNode
let overlayNode: ASImageNode
init() {
self.lineNode = ASImageNode()
self.overlayNode = ASImageNode()
}
}
private var currentColors: Colors?
private var currentConfiguration: Configuration?
private let foregroundLineNode: ASImageNode
private var backgroundLineNodes: [Int: BackgroundLineNode] = [:]
private var removingBackgroundLineNodes: [BackgroundLineNode] = []
private let maskContainerNode: ASDisplayNode
private let topShadowNode: ASImageNode
private let bottomShadowNode: ASImageNode
private let middleShadowNode: ASDisplayNode
private var currentForegroundImage: UIImage?
private var currentBackgroundImage: UIImage?
private var currentClearBackgroundImage: UIImage?
override public init() {
self.maskContainerNode = ASDisplayNode()
self.foregroundLineNode = ASImageNode()
self.topShadowNode = ASImageNode()
self.bottomShadowNode = ASImageNode()
self.middleShadowNode = ASDisplayNode()
self.middleShadowNode.backgroundColor = .white
super.init()
self.clipsToBounds = true
self.addSubnode(self.maskContainerNode)
self.addSubnode(self.foregroundLineNode)
self.maskContainerNode.addSubnode(self.topShadowNode)
self.maskContainerNode.addSubnode(self.bottomShadowNode)
self.maskContainerNode.addSubnode(self.middleShadowNode)
self.layer.mask = self.maskContainerNode.layer
}
public func update(colors: Colors, configuration: Configuration, transition: ContainedViewLayoutTransition) {
var transition = transition
let segmentSpacing: CGFloat = 2.0
if self.currentColors != colors {
self.currentColors = colors
self.currentForegroundImage = generateFilledCircleImage(diameter: 2.0, color: colors.foreground)?.resizableImage(withCapInsets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0), resizingMode: .stretch)
self.currentBackgroundImage = generateFilledCircleImage(diameter: 2.0, color: colors.background)?.resizableImage(withCapInsets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0), resizingMode: .stretch)
self.currentClearBackgroundImage = generateImage(CGSize(width: 2.0, height: 4.0 + segmentSpacing * 2.0 + 1.0 * 2.0), contextGenerator: { size, context in
context.setFillColor(colors.clearBackground.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.copy)
let ellipseFudge: CGFloat = 0.02
let topEllipse = CGRect(origin: CGPoint(x: -ellipseFudge, y: 1.0 + segmentSpacing), size: CGSize(width: 2.0 + ellipseFudge * 2.0, height: 2.0))
let bottomEllipse = CGRect(origin: CGPoint(x: -ellipseFudge, y: size.height - (1.0 + segmentSpacing) - 2.0), size: CGSize(width: 2.0 + ellipseFudge * 2.0, height: 2.0))
context.fillEllipse(in: topEllipse)
context.fillEllipse(in: bottomEllipse)
context.fill(CGRect(origin: CGPoint(x: 0.0, y: topEllipse.midY), size: CGSize(width: 2.0, height: bottomEllipse.midY - topEllipse.midY)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: 2.0, height: 2.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - 1.0), size: CGSize(width: 2.0, height: 2.0)))
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 1.0 + segmentSpacing + 2.0, left: 1.0, bottom: 1.0 + segmentSpacing + 2.0, right: 1.0), resizingMode: .stretch)
self.foregroundLineNode.image = self.currentForegroundImage
for (_, itemNode) in self.backgroundLineNodes {
itemNode.lineNode.image = self.currentBackgroundImage
itemNode.overlayNode.image = self.currentClearBackgroundImage
}
self.topShadowNode.image = generateImage(CGSize(width: 2.0, height: 7.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
var locations: [CGFloat] = [1.0, 0.0]
let colors: [CGColor] = [UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.cgColor]
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: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
self.bottomShadowNode.image = generateImage(CGSize(width: 2.0, height: 7.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
var locations: [CGFloat] = [1.0, 0.0]
let colors: [CGColor] = [UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.cgColor]
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: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
}
if self.currentConfiguration == nil {
transition = .immediate
}
if self.currentConfiguration != configuration {
var isCycledJump = false
if let currentConfiguration = self.currentConfiguration, currentConfiguration.count == configuration.count, currentConfiguration.index == 0, currentConfiguration.count > 4, configuration.index == configuration.count - 1 {
isCycledJump = true
}
self.currentConfiguration = configuration
let defaultVerticalInset: CGFloat = 7.0
let minSegmentHeight: CGFloat = 8.0
transition.updateFrame(node: self.topShadowNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: defaultVerticalInset)))
transition.updateFrame(node: self.bottomShadowNode, frame: CGRect(origin: CGPoint(x: 0.0, y: configuration.height - defaultVerticalInset), size: CGSize(width: 2.0, height: defaultVerticalInset)))
transition.updateFrame(node: self.middleShadowNode, frame: CGRect(origin: CGPoint(x: 0.0, y: defaultVerticalInset), size: CGSize(width: 2.0, height: configuration.height - defaultVerticalInset * 2.0)))
transition.updateFrame(node: self.maskContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: configuration.height)))
let availableVerticalHeight: CGFloat = configuration.height - defaultVerticalInset * 2.0
let proposedSegmentHeight: CGFloat = (availableVerticalHeight - segmentSpacing * CGFloat(configuration.count) + segmentSpacing) / CGFloat(configuration.count)
let segmentHeight = max(proposedSegmentHeight, minSegmentHeight)
let allItemsHeight = CGFloat(configuration.count) * segmentHeight + max(0.0, CGFloat(configuration.count - 1)) * segmentSpacing
var verticalInset = defaultVerticalInset
if allItemsHeight > availableVerticalHeight && allItemsHeight - 2.0 <= availableVerticalHeight {
verticalInset -= 2.0
}
let topItemsHeight = CGFloat(configuration.index) * (segmentHeight + segmentSpacing)
let bottomItemsHeight = allItemsHeight - topItemsHeight - segmentHeight
var itemScreenOffset = floorToScreenPixels((configuration.height - segmentHeight) / 2.0)
if itemScreenOffset - topItemsHeight > verticalInset {
itemScreenOffset = topItemsHeight + verticalInset
}
if itemScreenOffset + segmentHeight + bottomItemsHeight < configuration.height - verticalInset {
itemScreenOffset = configuration.height - verticalInset - (segmentHeight + bottomItemsHeight)
}
var backgroundItemNodesToOffset: [BackgroundLineNode] = []
var resolvedOffset: CGFloat = 0.0
func updateBackgroundLine(index: Int) -> Bool {
let indexDifference = index - configuration.index
let offsetDistance = CGFloat(indexDifference) * (segmentHeight + segmentSpacing)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemScreenOffset + offsetDistance), size: CGSize(width: 2.0, height: segmentHeight))
if itemFrame.maxY <= 0.0 || itemFrame.minY > configuration.height {
return false
}
var itemNodeTransition = transition
let itemNode: BackgroundLineNode
if let current = self.backgroundLineNodes[index] {
itemNode = current
let offset = itemFrame.minY - itemNode.lineNode.frame.minY
if abs(offset) > abs(resolvedOffset) {
resolvedOffset = offset
}
} else {
itemNodeTransition = .immediate
itemNode = BackgroundLineNode()
itemNode.lineNode.image = self.currentBackgroundImage
itemNode.overlayNode.image = self.currentClearBackgroundImage
self.backgroundLineNodes[index] = itemNode
self.insertSubnode(itemNode.lineNode, belowSubnode: self.foregroundLineNode)
self.topShadowNode.supernode?.insertSubnode(itemNode.overlayNode, belowSubnode: self.topShadowNode)
backgroundItemNodesToOffset.append(itemNode)
}
itemNodeTransition.updateFrame(node: itemNode.lineNode, frame: itemFrame, beginWithCurrentState: true)
itemNodeTransition.updateFrame(node: itemNode.overlayNode, frame: itemFrame.insetBy(dx: 0.0, dy: -(1.0 + segmentSpacing)), beginWithCurrentState: true)
return true
}
var validIndices = Set<Int>()
if configuration.index >= 0 {
for i in (0 ... configuration.index).reversed() {
if updateBackgroundLine(index: i) {
validIndices.insert(i)
} else {
break
}
}
}
if configuration.index < configuration.count {
for i in configuration.index + 1 ..< configuration.count {
if updateBackgroundLine(index: i) {
validIndices.insert(i)
} else {
break
}
}
}
if !resolvedOffset.isZero {
for itemNode in backgroundItemNodesToOffset {
transition.animatePositionAdditive(node: itemNode.lineNode, offset: CGPoint(x: 0.0, y: -resolvedOffset))
transition.animatePositionAdditive(node: itemNode.overlayNode, offset: CGPoint(x: 0.0, y: -resolvedOffset))
}
for itemNode in self.removingBackgroundLineNodes {
transition.animatePosition(node: itemNode.lineNode, to: CGPoint(x: 0.0, y: resolvedOffset), removeOnCompletion: false, additive: true)
transition.animatePosition(node: itemNode.overlayNode, to: CGPoint(x: 0.0, y: resolvedOffset), removeOnCompletion: false, additive: true)
}
}
var removeIndices: [Int] = []
for (index, itemNode) in self.backgroundLineNodes {
if !validIndices.contains(index) {
removeIndices.append(index)
if transition.isAnimated {
removingBackgroundLineNodes.append(itemNode)
transition.animatePosition(node: itemNode.overlayNode, to: CGPoint(x: 0.0, y: resolvedOffset), removeOnCompletion: false, additive: true)
transition.animatePosition(node: itemNode.lineNode, to: CGPoint(x: 0.0, y: resolvedOffset), removeOnCompletion: false, additive: true, completion: { [weak self, weak itemNode] _ in
guard let strongSelf = self, let itemNode = itemNode else {
return
}
strongSelf.removingBackgroundLineNodes.removeAll(where: { $0 === itemNode })
itemNode.lineNode.removeFromSupernode()
itemNode.overlayNode.removeFromSupernode()
})
} else {
itemNode.lineNode.removeFromSupernode()
itemNode.overlayNode.removeFromSupernode()
}
}
}
for index in removeIndices {
self.backgroundLineNodes.removeValue(forKey: index)
}
transition.updateFrame(node: self.foregroundLineNode, frame: CGRect(origin: CGPoint(x: 0.0, y: itemScreenOffset), size: CGSize(width: 2.0, height: segmentHeight)), beginWithCurrentState: true)
if transition.isAnimated && isCycledJump {
let duration: Double = 0.18
let maxOffset: CGFloat = -8.0
let offsetAnimation0 = self.layer.makeAnimation(from: 0.0 as NSNumber, to: maxOffset as NSNumber, keyPath: "bounds.origin.y", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: duration / 2.0, removeOnCompletion: false, additive: true, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
let offsetAnimation1 = strongSelf.layer.makeAnimation(from: maxOffset as NSNumber, to: 0.0 as NSNumber, keyPath: "bounds.origin.y", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: duration / 2.0, additive: true)
strongSelf.layer.add(offsetAnimation1, forKey: "cycleShake")
})
self.layer.add(offsetAnimation0, forKey: "cycleShake")
}
}
}
}
+44
View File
@@ -0,0 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
config_setting(
name = "debug_build",
values = {
"compilation_mode": "dbg",
},
)
optimization_flags = select({
":debug_build": [
#"-O",
],
"//conditions:default": [],
})
swift_library(
name = "AnimatedStickerNode",
module_name = "AnimatedStickerNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
] + optimization_flags,
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/YuvConversion:YuvConversion",
"//submodules/GZip:GZip",
"//submodules/rlottie:RLottieBinding",
"//submodules/MediaResources:MediaResources",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/ManagedFile:ManagedFile",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AnimationCompression:AnimationCompression",
"//submodules/Components/MetalImageView:MetalImageView",
"//submodules/WebPBinding:WebPBinding",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,641 @@
import Foundation
import Compression
import Display
import SwiftSignalKit
import MediaResources
import RLottieBinding
import GZip
import ManagedFile
import AnimationCompression
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
public extension EmojiFitzModifier {
var lottieFitzModifier: LottieFitzModifier {
switch self {
case .type12:
return .type12
case .type3:
return .type3
case .type4:
return .type4
case .type5:
return .type5
case .type6:
return .type6
}
}
}
public protocol AnimatedStickerFrameSource: AnyObject {
var frameRate: Int { get }
var frameCount: Int { get }
var frameIndex: Int { get }
func takeFrame(draw: Bool) -> AnimatedStickerFrame?
func skipToEnd()
func skipToFrameIndex(_ index: Int)
}
final class AnimatedStickerFrameSourceWrapper {
let value: AnimatedStickerFrameSource
init(_ value: AnimatedStickerFrameSource) {
self.value = value
}
}
public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource {
private let queue: Queue
private var data: Data
private var dataComplete: Bool
private let notifyUpdated: () -> Void
private var scratchBuffer: Data
let width: Int
let bytesPerRow: Int
let height: Int
public let frameRate: Int
public let frameCount: Int
public var frameIndex: Int
private let initialOffset: Int
private var offset: Int
var decodeBuffer: Data
var frameBuffer: Data
public init?(queue: Queue, data: Data, complete: Bool, notifyUpdated: @escaping () -> Void) {
self.queue = queue
self.data = data
self.dataComplete = complete
self.notifyUpdated = notifyUpdated
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
var offset = 0
var width = 0
var height = 0
var bytesPerRow = 0
var frameRate = 0
var frameCount = 0
if !self.data.withUnsafeBytes({ buffer -> Bool in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return false
}
var frameRateValue: Int32 = 0
var frameCountValue: Int32 = 0
var widthValue: Int32 = 0
var heightValue: Int32 = 0
var bytesPerRowValue: Int32 = 0
memcpy(&frameRateValue, bytes.advanced(by: offset), 4)
offset += 4
memcpy(&frameCountValue, bytes.advanced(by: offset), 4)
offset += 4
memcpy(&widthValue, bytes.advanced(by: offset), 4)
offset += 4
memcpy(&heightValue, bytes.advanced(by: offset), 4)
offset += 4
memcpy(&bytesPerRowValue, bytes.advanced(by: offset), 4)
offset += 4
frameRate = Int(frameRateValue)
frameCount = Int(frameCountValue)
width = Int(widthValue)
height = Int(heightValue)
bytesPerRow = Int(bytesPerRowValue)
return true
}) {
return nil
}
self.bytesPerRow = bytesPerRow
self.width = width
self.height = height
self.frameRate = frameRate
self.frameCount = frameCount
self.frameIndex = 0
self.initialOffset = offset
self.offset = offset
self.decodeBuffer = Data(count: self.bytesPerRow * height)
self.frameBuffer = Data(count: self.bytesPerRow * height)
let frameBufferLength = self.frameBuffer.count
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memset(bytes, 0, frameBufferLength)
}
}
deinit {
assert(self.queue.isCurrent())
}
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
var frameData: Data?
var isLastFrame = false
let dataLength = self.data.count
let decodeBufferLength = self.decodeBuffer.count
let frameBufferLength = self.frameBuffer.count
let frameIndex = self.frameIndex
self.data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
if self.offset + 4 > dataLength {
if self.dataComplete {
self.frameIndex = 0
self.offset = self.initialOffset
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memset(bytes, 0, frameBufferLength)
}
}
return
}
var frameLength: Int32 = 0
memcpy(&frameLength, bytes.advanced(by: self.offset), 4)
if self.offset + 4 + Int(frameLength) > dataLength {
return
}
self.offset += 4
if draw {
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.frameBuffer.withUnsafeMutableBytes { frameBuffer -> Void in
guard let frameBytes = frameBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self)
var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self)
for _ in 0 ..< decodeBufferLength / 8 {
lhs.pointee = lhs.pointee ^ rhs.pointee
lhs = lhs.advanced(by: 1)
rhs = rhs.advanced(by: 1)
}
var lhsRest = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
var rhsRest = UnsafeMutableRawPointer(decodeBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
for _ in (decodeBufferLength / 8) * 8 ..< decodeBufferLength {
lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee
lhsRest = lhsRest.advanced(by: 1)
rhsRest = rhsRest.advanced(by: 1)
}
frameData = Data(bytes: frameBytes, count: decodeBufferLength)
}
}
}
}
self.frameIndex += 1
self.offset += Int(frameLength)
if self.offset == dataLength && self.dataComplete {
isLastFrame = true
self.frameIndex = 0
self.offset = self.initialOffset
self.frameBuffer.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memset(bytes, 0, frameBufferLength)
}
}
}
if let frameData = frameData, draw {
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame, totalFrames: self.frameCount)
} else {
return nil
}
}
func updateData(data: Data, complete: Bool) {
self.data = data
self.dataComplete = complete
}
public func skipToEnd() {
}
public func skipToFrameIndex(_ index: Int) {
}
}
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
private final class AnimatedStickerDirectFrameSourceCache {
private enum FrameRangeResult {
case range(Range<Int>)
case notFound
case corruptedFile
}
private let queue: Queue
private let storeQueue: Queue
private let file: ManagedFile
private let frameCount: Int
private let width: Int
private let height: Int
private let useHardware: Bool
private var isStoringFrames = Set<Int>()
private var scratchBuffer: Data
private var decodeBuffer: Data
private var frameCompressor: AnimationCompressor?
init?(queue: Queue, pathPrefix: String, width: Int, height: Int, frameCount: Int, fitzModifier: EmojiFitzModifier?, useHardware: Bool) {
self.queue = queue
self.storeQueue = sharedStoreQueue
self.frameCount = frameCount
self.width = width// alignUp(size: width, align: 8)
self.height = height//alignUp(size: height, align: 8)
self.useHardware = useHardware
let suffix : String
if let fitzModifier = fitzModifier {
suffix = "_fitz\(fitzModifier.rawValue)"
} else {
suffix = ""
}
let path = "\(pathPrefix)_\(width):\(height)\(suffix).stickerframecachev3\(useHardware ? "-mtl" : "")"
var file = ManagedFile(queue: queue, path: path, mode: .readwrite)
if let file = file {
self.file = file
} else {
let _ = try? FileManager.default.removeItem(atPath: path)
file = ManagedFile(queue: queue, path: path, mode: .readwrite)
if let file = file {
self.file = file
} else {
return nil
}
}
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
self.decodeBuffer = Data(count: yuvaLength)
self.initializeFrameTable()
}
private func initializeFrameTable() {
if let size = self.file.getSize(), size >= self.frameCount * 4 * 2 {
} else {
self.file.truncate(count: 0)
for _ in 0 ..< self.frameCount {
var zero: Int32 = 0
let _ = self.file.write(&zero, count: 4)
let _ = self.file.write(&zero, count: 4)
}
}
}
private func readFrameRange(index: Int) -> FrameRangeResult {
if index < 0 || index >= self.frameCount {
return .notFound
}
let _ = self.file.seek(position: Int64(index * 4 * 2))
var offset: Int32 = 0
var length: Int32 = 0
if self.file.read(&offset, 4) != 4 {
return .corruptedFile
}
if self.file.read(&length, 4) != 4 {
return .corruptedFile
}
if length == 0 {
return .notFound
}
if length < 0 || offset < 0 {
return .corruptedFile
}
if Int64(offset) + Int64(length) > 200 * 1024 * 1024 {
return .corruptedFile
}
return .range(Int(offset) ..< Int(offset + length))
}
func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
if self.useHardware {
self.storeUncompressedRgbFrameMetal(index: index, rgbData: rgbData)
} else {
self.storeUncompressedRgbFrameSoft(index: index, rgbData: rgbData)
}
}
func storeUncompressedRgbFrameMetal(index: Int, rgbData: Data) {
if self.isStoringFrames.contains(index) {
return
}
self.isStoringFrames.insert(index)
if self.frameCompressor == nil {
self.frameCompressor = AnimationCompressor(sharedContext: AnimationCompressor.SharedContext.shared)
}
let queue = self.queue
let frameCompressor = self.frameCompressor
let width = self.width
let height = self.height
DispatchQueue.main.async { [weak self] in
frameCompressor?.compress(image: AnimationCompressor.ImageData(width: width, height: height, bytesPerRow: width * 4, data: rgbData), completion: { compressedData in
queue.async {
guard let strongSelf = self else {
return
}
guard let currentSize = strongSelf.file.getSize() else {
return
}
let _ = strongSelf.file.seek(position: Int64(index * 4 * 2))
var offset = Int32(currentSize)
var length = Int32(compressedData.data.count)
let _ = strongSelf.file.write(&offset, count: 4)
let _ = strongSelf.file.write(&length, count: 4)
let _ = strongSelf.file.seek(position: Int64(currentSize))
compressedData.data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
if let baseAddress = buffer.baseAddress {
let _ = strongSelf.file.write(baseAddress, count: Int(length))
}
}
}
})
}
}
func storeUncompressedRgbFrameSoft(index: Int, rgbData: Data) {
if index < 0 || index >= self.frameCount {
return
}
if self.isStoringFrames.contains(index) {
return
}
self.isStoringFrames.insert(index)
let width = self.width
let height = self.height
let queue = self.queue
self.storeQueue.async { [weak self] in
let compressedData = compressFrame(width: width, height: height, rgbData: rgbData, unpremultiply: true)
queue.async {
guard let strongSelf = self else {
return
}
guard let currentSize = strongSelf.file.getSize() else {
return
}
guard let compressedData = compressedData else {
return
}
let _ = strongSelf.file.seek(position: Int64(index * 4 * 2))
var offset = Int32(currentSize)
var length = Int32(compressedData.count)
let _ = strongSelf.file.write(&offset, count: 4)
let _ = strongSelf.file.write(&length, count: 4)
let _ = strongSelf.file.seek(position: Int64(currentSize))
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
if let baseAddress = buffer.baseAddress {
let _ = strongSelf.file.write(baseAddress, count: Int(length))
}
}
}
}
}
/*func readUncompressedYuvaFrameOld(index: Int) -> Data? {
if index < 0 || index >= self.frameCount {
return nil
}
let rangeResult = self.readFrameRange(index: index)
switch rangeResult {
case let .range(range):
self.file.seek(position: Int64(range.lowerBound))
let length = range.upperBound - range.lowerBound
let compressedData = self.file.readData(count: length)
if compressedData.count != length {
return nil
}
var frameData: Data?
let decodeBufferLength = self.decodeBuffer.count
compressedData.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
frameData = Data(bytes: decodeBytes, count: resultLength)
}
}
}
return frameData
case .notFound:
return nil
case .corruptedFile:
self.file.truncate(count: 0)
self.initializeFrameTable()
return nil
}
}*/
func readCompressedFrame(index: Int, totalFrames: Int) -> AnimatedStickerFrame? {
if index < 0 || index >= self.frameCount {
return nil
}
let rangeResult = self.readFrameRange(index: index)
switch rangeResult {
case let .range(range):
let _ = self.file.seek(position: Int64(range.lowerBound))
let length = range.upperBound - range.lowerBound
let compressedData = self.file.readData(count: length)
if compressedData.count != length {
return nil
}
if compressedData.count > 4 {
var magic: Int32 = 0
compressedData.withUnsafeBytes { bytes in
let _ = memcpy(&magic, bytes.baseAddress!, 4)
}
if magic == 0x543ee445 {
return AnimatedStickerFrame(data: compressedData, type: .dct, width: 0, height: 0, bytesPerRow: 0, index: index, isLastFrame: index == frameCount - 1, totalFrames: frameCount)
}
}
var frameData: Data?
let decodeBufferLength = self.decodeBuffer.count
compressedData.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
frameData = Data(bytes: decodeBytes, count: resultLength)
}
}
}
if let frameData = frameData {
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: index, isLastFrame: index == frameCount - 1, totalFrames: frameCount)
} else {
return nil
}
case .notFound:
return nil
case .corruptedFile:
self.file.truncate(count: 0)
self.initializeFrameTable()
return nil
}
}
}
public final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
private let queue: Queue
private let data: Data
private let width: Int
private let height: Int
private let cache: AnimatedStickerDirectFrameSourceCache?
private let bytesPerRow: Int
public let frameCount: Int
public let frameRate: Int
fileprivate var currentFrame: Int
private let animation: LottieInstance
public var frameIndex: Int {
return self.currentFrame % self.frameCount
}
public init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?, useMetalCache: Bool = false, fitzModifier: EmojiFitzModifier?) {
self.queue = queue
self.data = data
self.width = width
self.height = height
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width))
self.currentFrame = 0
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
guard let animation = LottieInstance(data: decompressedData, fitzModifier: fitzModifier?.lottieFitzModifier ?? .none, colorReplacements: nil, cacheKey: "") else {
print("Could not load sticker data")
return nil
}
self.animation = animation
let frameCount = max(1, Int(animation.frameCount))
self.frameCount = frameCount
self.frameRate = max(1, Int(animation.frameRate))
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount, fitzModifier: fitzModifier, useHardware: useMetalCache)
}
}
deinit {
assert(self.queue.isCurrent())
}
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
let frameIndex = self.currentFrame % self.frameCount
self.currentFrame += 1
if draw {
if let cache = self.cache, let compressedFrame = cache.readCompressedFrame(index: frameIndex, totalFrames: self.frameCount) {
return compressedFrame
} else {
var frameData = Data(count: self.bytesPerRow * self.height)
frameData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
memset(bytes, 0, self.bytesPerRow * self.height)
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow))
}
if let cache = self.cache {
cache.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
}
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
}
} else {
return nil
}
}
public func skipToEnd() {
self.currentFrame = self.frameCount - 1
}
public func skipToFrameIndex(_ index: Int) {
self.currentFrame = index
}
}
@@ -0,0 +1,848 @@
import Foundation
import SwiftSignalKit
import Compression
import Display
import AsyncDisplayKit
import YuvConversion
import MediaResources
import AnimationCompression
import UIKit
private let sharedQueue = Queue()
private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
private var value: Bool = false
var updated: ((Bool) -> Void)?
override init() {
super.init()
self.isLayerBacked = true
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
if !self.value {
self.value = true
self.updated?(true)
}
}
override func didExitHierarchy() {
super.didExitHierarchy()
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else {
return
}
if !strongSelf.isInHierarchy {
if strongSelf.value {
strongSelf.value = false
strongSelf.updated?(false)
}
}
}
}
}
public enum AnimatedStickerMode {
case cached
case direct(cachePathPrefix: String?)
}
public enum AnimatedStickerPlaybackPosition {
case start
case end
case timestamp(Double)
case frameIndex(Int)
}
public enum AnimatedStickerPlaybackMode {
case once
case count(Int)
case loop
case still(AnimatedStickerPlaybackPosition)
}
public final class AnimatedStickerFrame {
public let data: Data
public let type: AnimationRendererFrameType
public let width: Int
public let height: Int
public let bytesPerRow: Int
let index: Int
let isLastFrame: Bool
let totalFrames: Int
let multiplyAlpha: Bool
init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool, totalFrames: Int, multiplyAlpha: Bool = false) {
self.data = data
self.type = type
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
self.index = index
self.isLastFrame = isLastFrame
self.totalFrames = totalFrames
self.multiplyAlpha = multiplyAlpha
}
}
public final class AnimatedStickerFrameQueue {
private let queue: Queue
private let length: Int
private let source: AnimatedStickerFrameSource
private var frames: [AnimatedStickerFrame] = []
public init(queue: Queue, length: Int, source: AnimatedStickerFrameSource) {
self.queue = queue
self.length = length
self.source = source
}
deinit {
assert(self.queue.isCurrent())
}
public func take(draw: Bool) -> AnimatedStickerFrame? {
if self.frames.isEmpty {
if let frame = self.source.takeFrame(draw: draw) {
self.frames.append(frame)
}
}
if !self.frames.isEmpty {
let frame = self.frames.removeFirst()
return frame
} else {
return nil
}
}
public func generateFramesIfNeeded() {
if self.frames.isEmpty {
if let frame = self.source.takeFrame(draw: true) {
self.frames.append(frame)
}
}
}
}
public struct AnimatedStickerStatus: Equatable {
public let playing: Bool
public let duration: Double
public let timestamp: Double
public init(playing: Bool, duration: Double, timestamp: Double) {
self.playing = playing
self.duration = duration
self.timestamp = timestamp
}
}
public protocol AnimatedStickerNodeSource {
var fitzModifier: EmojiFitzModifier? { get }
var isVideo: Bool { get }
func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError>
func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError>
}
public protocol AnimatedStickerNode: ASDisplayNode {
var automaticallyLoadFirstFrame: Bool { get set }
var automaticallyLoadLastFrame: Bool { get set }
var playToCompletionOnStop: Bool { get set }
var started: () -> Void { get set }
var completed: (Bool) -> Void { get set }
var frameUpdated: (Int, Int) -> Void { get set }
var currentFrameIndex: Int { get }
var currentFrameImage: UIImage? { get }
var currentFrameCount: Int { get }
var isPlaying: Bool { get }
var stopAtNearestLoop: Bool { get set }
var status: Signal<AnimatedStickerStatus, NoError> { get }
var autoplay: Bool { get set }
var visibility: Bool { get set }
var overrideVisibility: Bool { get set }
var isPlayingChanged: (Bool) -> Void { get set }
func cloneCurrentFrame(from otherNode: AnimatedStickerNode?)
func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode)
func reset()
func playOnce()
func playLoop()
func play(firstFrame: Bool, fromIndex: Int?)
func pause()
func stop()
func seekTo(_ position: AnimatedStickerPlaybackPosition)
func playIfNeeded() -> Bool
func updateLayout(size: CGSize)
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool)
}
public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedStickerNode {
private let queue: Queue
private let disposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private let eventsNode: AnimatedStickerNodeDisplayEvents
public var automaticallyLoadFirstFrame: Bool = false
public var automaticallyLoadLastFrame: Bool = false
public var playToCompletionOnStop: Bool = false
public var started: () -> Void = {}
private var reportedStarted = false
public var completed: (Bool) -> Void = { _ in }
public var frameUpdated: (Int, Int) -> Void = { _, _ in }
public private(set) var currentFrameIndex: Int = 0
public private(set) var currentFrameCount: Int = 0
public private(set) var currentFrameRate: Int = 0
private var playFromIndex: Int?
public var frameColorUpdated: ((UIColor) -> Void)?
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
private var directData: (Data, String, Int, Int, String?, EmojiFitzModifier?, Bool)?
private var cachedData: (Data, Bool, EmojiFitzModifier?)?
private let useMetalCache: Bool
private var renderer: AnimationRendererPool.Holder?
public var isPlaying: Bool = false
private var currentLoopCount: Int = 0
private var canDisplayFirstFrame: Bool = false
public var playbackMode: AnimatedStickerPlaybackMode = .loop
public var stopAtNearestLoop: Bool = false
private let playbackStatus = Promise<AnimatedStickerStatus>()
public var status: Signal<AnimatedStickerStatus, NoError> {
return self.playbackStatus.get()
}
public var autoplay = false
public var overrideVisibility: Bool = false
public var currentFrameImage: UIImage? {
return self.renderer?.renderer.currentFrameImage
}
public var visibility = false {
didSet {
if self.visibility != oldValue {
self.updateIsPlaying()
}
}
}
private var isDisplaying = false {
didSet {
if self.isDisplaying != oldValue {
self.updateIsPlaying()
}
}
}
public var isPlayingChanged: (Bool) -> Void = { _ in }
private var overlayColor: (UIColor?, Bool)? = nil
private var size: CGSize?
public var dynamicColor: UIColor? {
didSet {
if let renderer = self.renderer?.renderer as? SoftwareAnimationRenderer {
renderer.renderAsTemplateImage = self.dynamicColor != nil
}
self.renderer?.renderer.view.tintColor = self.dynamicColor
}
}
public var forceSynchronous = false
public init(useMetalCache: Bool = false) {
self.queue = sharedQueue
self.eventsNode = AnimatedStickerNodeDisplayEvents()
self.useMetalCache = useMetalCache
super.init()
self.eventsNode.updated = { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.isDisplaying = value
}
self.addSubnode(self.eventsNode)
}
deinit {
self.disposable.dispose()
self.fetchDisposable.dispose()
self.timer.swap(nil)?.invalidate()
}
private static let hardwareRendererPool = AnimationRendererPool(generate: {
if #available(iOS 10.0, *) {
return CompressedAnimationRenderer()
} else {
return SoftwareAnimationRenderer(templateImageSupport: true)
}
})
private static let softwareRendererPool = AnimationRendererPool(generate: {
return SoftwareAnimationRenderer(templateImageSupport: true)
})
private weak var nodeToCopyFrameFrom: DefaultAnimatedStickerNodeImpl?
override public func didLoad() {
super.didLoad()
if #available(iOS 10.0, *), (self.useMetalCache/* || "".isEmpty*/) {
self.renderer = DefaultAnimatedStickerNodeImpl.hardwareRendererPool.take()
} else {
self.renderer = DefaultAnimatedStickerNodeImpl.softwareRendererPool.take()
if let renderer = self.renderer?.renderer as? SoftwareAnimationRenderer {
renderer.renderAsTemplateImage = self.dynamicColor != nil
}
self.renderer?.renderer.view.tintColor = self.dynamicColor
if let contents = self.nodeToCopyFrameFrom?.renderer?.renderer.contents {
self.renderer?.renderer.contents = contents
}
}
self.renderer?.renderer.frame = CGRect(origin: CGPoint(), size: self.size ?? self.bounds.size)
if let (overlayColor, replace) = self.overlayColor {
self.renderer?.renderer.setOverlayColor(overlayColor, replace: replace, animated: false)
}
self.nodeToCopyFrameFrom = nil
self.addSubnode(self.renderer!.renderer)
}
public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) {
guard let otherNode = otherNode as? DefaultAnimatedStickerNodeImpl else {
self.nodeToCopyFrameFrom = nil
return
}
if let renderer = self.renderer?.renderer as? SoftwareAnimationRenderer, let otherRenderer = otherNode.renderer?.renderer as? SoftwareAnimationRenderer {
if let contents = otherRenderer.contents {
renderer.contents = contents
}
} else {
self.nodeToCopyFrameFrom = otherNode
}
}
public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) {
if width < 2 || height < 2 {
return
}
self.playbackMode = playbackMode
switch mode {
case let .direct(cachePathPrefix):
let f: (String) -> Void = { [weak self] path in
guard let strongSelf = self else {
return
}
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
strongSelf.directData = (directData, path, width, height, cachePathPrefix, source.fitzModifier, source.isVideo)
}
if case let .still(position) = strongSelf.playbackMode {
strongSelf.seekTo(position)
} else if strongSelf.isPlaying || strongSelf.autoplay {
if strongSelf.autoplay {
strongSelf.isSetUpForPlayback = false
strongSelf.isPlaying = true
}
let fromIndex = strongSelf.playFromIndex
strongSelf.playFromIndex = nil
strongSelf.play(fromIndex: fromIndex)
} else if strongSelf.canDisplayFirstFrame {
strongSelf.play(firstFrame: true)
}
}
self.disposable.set((source.directDataPath(attemptSynchronously: self.forceSynchronous)
|> filter { $0 != nil }
|> deliverOnMainQueue).startStrict(next: { path in
f(path!)
}))
case .cached:
self.disposable.set((source.cachedDataPath(width: width, height: height)
|> deliverOnMainQueue).startStrict(next: { [weak self] path, complete in
guard let strongSelf = self else {
return
}
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
if let (_, currentComplete, _) = strongSelf.cachedData {
if !currentComplete {
strongSelf.cachedData = (data, complete, source.fitzModifier)
strongSelf.frameSource.with { frameSource in
frameSource?.with { frameSource in
if let frameSource = frameSource.value as? AnimatedStickerCachedFrameSource {
frameSource.updateData(data: data, complete: complete)
}
}
}
}
} else {
strongSelf.cachedData = (data, complete, source.fitzModifier)
if strongSelf.isPlaying {
strongSelf.play()
} else if strongSelf.canDisplayFirstFrame {
strongSelf.play(firstFrame: true)
}
}
}
}))
}
}
public func reset() {
self.disposable.set(nil)
self.fetchDisposable.set(nil)
}
private func updateIsPlaying() {
if !self.autoplay {
let isPlaying = self.visibility && (self.isDisplaying || self.overrideVisibility)
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
if isPlaying {
self.play()
} else{
self.pause()
}
self.isPlayingChanged(isPlaying)
}
}
if self.automaticallyLoadLastFrame {
if self.isDisplaying {
self.seekTo(.end)
}
} else {
let canDisplayFirstFrame = self.automaticallyLoadFirstFrame && self.isDisplaying
if self.canDisplayFirstFrame != canDisplayFirstFrame {
self.canDisplayFirstFrame = canDisplayFirstFrame
if canDisplayFirstFrame {
self.play(firstFrame: true)
}
}
}
}
private var isSetUpForPlayback = false
public func playOnce() {
self.playbackMode = .once
self.play()
}
public func playLoop() {
self.playbackMode = .loop
self.play()
}
public func play(firstFrame: Bool = false, fromIndex: Int? = nil) {
if !firstFrame {
switch self.playbackMode {
case .once:
self.isPlaying = true
case .count:
self.currentLoopCount = 0
self.isPlaying = true
default:
break
}
}
if self.isSetUpForPlayback {
let directData = self.directData
let cachedData = self.cachedData
let queue = self.queue
let timerHolder = self.timer
let frameSourceHolder = self.frameSource
let useMetalCache = self.useMetalCache
self.queue.async { [weak self] in
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }.value
if maybeFrameSource == nil {
let notifyUpdated: (() -> Void)? = nil
if let directData = directData {
if directData.6 {
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
} else {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
}
} else if let (cachedData, cachedDataComplete, _) = cachedData {
if #available(iOS 9.0, *) {
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
notifyUpdated?()
})
}
}
let _ = frameSourceHolder.swap(maybeFrameSource.flatMap { maybeFrameSource in
return QueueLocalObject(queue: queue, generate: {
return AnimatedStickerFrameSourceWrapper(maybeFrameSource)
})
})
}
guard let frameSource = maybeFrameSource else {
return
}
if let fromIndex = fromIndex {
frameSource.skipToFrameIndex(fromIndex)
}
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
})
timerHolder.swap(nil)?.invalidate()
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
let frameRate = frameSource.frameRate
let timerEvent: () -> Void = {
let frame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: true)
}
if let frame = frame {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
guard let strongSelf = self else {
return
}
if !strongSelf.reportedStarted {
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.frameUpdated(frame.index, frame.totalFrames)
strongSelf.currentFrameIndex = frame.index
strongSelf.currentFrameCount = frame.totalFrames
strongSelf.currentFrameRate = frameRate
if frame.isLastFrame {
var stopped = false
var stopNow = false
if case .still = strongSelf.playbackMode {
stopNow = true
} else if case .once = strongSelf.playbackMode {
stopNow = true
} else if case let .count(count) = strongSelf.playbackMode {
strongSelf.currentLoopCount += 1
if count <= strongSelf.currentLoopCount {
stopNow = true
}
} else if strongSelf.stopAtNearestLoop {
stopNow = true
}
if stopNow {
strongSelf.stop()
strongSelf.isPlaying = false
stopped = true
}
strongSelf.completed(stopped)
}
let timestamp: Double = frameRate > 0 ? Double(frame.index) / Double(frameRate) : 0
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: strongSelf.isPlaying, duration: duration, timestamp: timestamp)))
}
}
frameQueue.with { frameQueue in
frameQueue.generateFramesIfNeeded()
}
}
let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
timerEvent()
}, queue: queue)
let _ = timerHolder.swap(timer)
timerEvent()
timer.start()
}
} else {
self.isSetUpForPlayback = true
let directData = self.directData
let cachedData = self.cachedData
if directData == nil && cachedData == nil {
self.playFromIndex = fromIndex
}
let queue = self.queue
let timerHolder = self.timer
let frameSourceHolder = self.frameSource
let useMetalCache = self.useMetalCache
self.queue.async { [weak self] in
var maybeFrameSource: AnimatedStickerFrameSource?
let notifyUpdated: (() -> Void)? = nil
if let directData = directData {
if directData.6 {
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
} else {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
}
} else if let (cachedData, cachedDataComplete, _) = cachedData {
if #available(iOS 9.0, *) {
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
notifyUpdated?()
})
}
}
let _ = frameSourceHolder.swap(maybeFrameSource.flatMap { maybeFrameSource in
return QueueLocalObject(queue: queue, generate: {
return AnimatedStickerFrameSourceWrapper(maybeFrameSource)
})
})
guard let frameSource = maybeFrameSource else {
return
}
if let fromIndex = fromIndex {
frameSource.skipToFrameIndex(fromIndex)
}
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
})
timerHolder.swap(nil)?.invalidate()
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
let frameRate = frameSource.frameRate
let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
let frame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: true)
}
if let frame = frame {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
guard let strongSelf = self else {
return
}
if !strongSelf.reportedStarted {
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.frameUpdated(frame.index, frame.totalFrames)
strongSelf.currentFrameIndex = frame.index
strongSelf.currentFrameCount = frame.totalFrames
strongSelf.currentFrameRate = frameRate
if frame.isLastFrame {
var stopped = false
var stopNow = false
if case .still = strongSelf.playbackMode {
stopNow = true
} else if case .once = strongSelf.playbackMode {
stopNow = true
} else if case let .count(count) = strongSelf.playbackMode {
strongSelf.currentLoopCount += 1
if count <= strongSelf.currentLoopCount {
stopNow = true
}
} else if strongSelf.stopAtNearestLoop {
stopNow = true
}
if stopNow {
strongSelf.stop()
strongSelf.isPlaying = false
stopped = true
}
strongSelf.completed(stopped)
}
let timestamp: Double = frameRate > 0 ? Double(frame.index) / Double(frameRate) : 0
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: strongSelf.isPlaying, duration: duration, timestamp: timestamp)))
}
}
frameQueue.with { frameQueue in
frameQueue.generateFramesIfNeeded()
}
}, queue: queue)
let _ = timerHolder.swap(timer)
timer.start()
}
}
}
public func pause() {
self.timer.swap(nil)?.invalidate()
}
public func stop() {
self.isSetUpForPlayback = false
self.reportedStarted = false
self.timer.swap(nil)?.invalidate()
if self.playToCompletionOnStop {
self.seekTo(.start)
}
}
public func seekTo(_ position: AnimatedStickerPlaybackPosition) {
self.isPlaying = false
let directData = self.directData
let cachedData = self.cachedData
let queue = self.queue
let frameSourceHolder = self.frameSource
let timerHolder = self.timer
let useMetalCache = self.useMetalCache
let action = { [weak self] in
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }.value
if case .timestamp = position {
} else {
if let directData = directData {
if directData.6 {
maybeFrameSource = VideoStickerDirectFrameSource(queue: queue, path: directData.1, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
} else {
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4, useMetalCache: useMetalCache, fitzModifier: directData.5)
}
if case .end = position {
maybeFrameSource?.skipToEnd()
}
} else if let (cachedData, cachedDataComplete, _) = cachedData {
if #available(iOS 9.0, *) {
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {})
}
}
}
guard let frameSource = maybeFrameSource else {
return
}
if frameSource.frameCount == 0 {
return
}
let frameQueue = QueueLocalObject<AnimatedStickerFrameQueue>(queue: queue, generate: {
return AnimatedStickerFrameQueue(queue: queue, length: 1, source: frameSource)
})
timerHolder.swap(nil)?.invalidate()
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
var maybeFrame: AnimatedStickerFrame??
if case let .timestamp(timestamp) = position {
var stickerTimestamp = timestamp
while stickerTimestamp > duration {
stickerTimestamp -= duration
}
let targetFrame = Int(stickerTimestamp / duration * Double(frameSource.frameCount))
if targetFrame == frameSource.frameIndex {
return
}
var delta = targetFrame - frameSource.frameIndex
if delta < 0 {
delta = frameSource.frameCount + delta
}
for i in 0 ..< delta {
maybeFrame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: i == delta - 1)
}
}
} else if case let .frameIndex(frameIndex) = position {
let targetFrame = frameIndex
if targetFrame == frameSource.frameIndex {
return
}
var delta = targetFrame - frameSource.frameIndex
if delta < 0 {
delta = frameSource.frameCount + delta
}
for i in 0 ..< delta {
maybeFrame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: i == delta - 1)
}
}
} else {
maybeFrame = frameQueue.syncWith { frameQueue in
return frameQueue.take(draw: true)
}
}
if let maybeFrame = maybeFrame, let frame = maybeFrame {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.renderer?.renderer.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, mulAlpha: frame.multiplyAlpha, completion: {
guard let strongSelf = self else {
return
}
if !strongSelf.reportedStarted {
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: false, duration: duration, timestamp: 0.0)))
}
}
frameQueue.with { frameQueue in
frameQueue.generateFramesIfNeeded()
}
}
if self.forceSynchronous {
action()
} else {
self.queue.async(action)
}
}
public func playIfNeeded() -> Bool {
if !self.isPlaying {
self.isPlaying = true
self.play()
return true
}
return false
}
public func updateLayout(size: CGSize) {
self.size = size
self.renderer?.renderer.frame = CGRect(origin: CGPoint(), size: size)
}
public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
self.overlayColor = (color, replace)
self.renderer?.renderer.setOverlayColor(color, replace: replace, animated: animated)
}
}
@@ -0,0 +1,58 @@
import Foundation
import SwiftSignalKit
import UIKit
import AsyncDisplayKit
public enum AnimationRendererFrameType {
case argb
case yuva
case dct
}
final class AnimationRendererPool {
final class Holder {
let pool: AnimationRendererPool
let renderer: AnimationRenderer
init(pool: AnimationRendererPool, renderer: AnimationRenderer) {
self.pool = pool
self.renderer = renderer
}
deinit {
self.renderer.removeFromSupernode()
self.pool.putBack(renderer: self.renderer)
}
}
private let generate: () -> AnimationRenderer
private var items: [AnimationRenderer] = []
init(generate: @escaping () -> AnimationRenderer) {
self.generate = generate
}
func take() -> Holder {
if !self.items.isEmpty {
let item = self.items.removeLast()
return Holder(pool: self, renderer: item)
} else {
return Holder(pool: self, renderer: self.generate())
}
}
private func putBack(renderer: AnimationRenderer) {
/*#if DEBUG
self.items.append(renderer)
#endif*/
}
}
protocol AnimationRenderer: ASDisplayNode {
var currentFrameImage: UIImage? { get }
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?)
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool)
}
@@ -0,0 +1,134 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import YuvConversion
import Accelerate
import AnimationCompression
import Metal
import MetalKit
import MetalImageView
@available(iOS 10.0, *)
final class CompressedAnimationRenderer: ASDisplayNode, AnimationRenderer {
private final class View: UIView {
static override var layerClass: AnyClass {
return MetalImageLayer.self
}
init(device: MTLDevice) {
super.init(frame: CGRect())
(self.layer as! MetalImageLayer).renderer.device = device
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
}
private var highlightedContentNode: ASDisplayNode?
private var highlightedColor: UIColor?
private var highlightReplacesContent = false
private let renderer: CompressedImageRenderer
var currentFrameImage: UIImage? {
return nil
}
override init() {
self.renderer = CompressedImageRenderer(sharedContext: AnimationCompressor.SharedContext.shared)!
super.init()
self.setViewBlock({
return View(device: AnimationCompressor.SharedContext.shared.device)
})
}
override func didLoad() {
super.didLoad()
self.layer.isOpaque = false
self.layer.backgroundColor = nil
}
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?) {
switch type {
case .dct:
self.renderer.renderIdct(layer: self.layer as! MetalImageLayer, compressedImage: AnimationCompressor.CompressedImageData(data: data), completion: { [weak self] in
self?.updateHighlightedContentNode()
completion()
})
case .argb:
self.renderer.renderRgb(layer: self.layer as! MetalImageLayer, width: width, height: height, bytesPerRow: bytesPerRow, data: data, completion: { [weak self] in
self?.updateHighlightedContentNode()
completion()
})
case .yuva:
self.renderer.renderYuva(layer: self.layer as! MetalImageLayer, width: width, height: height, data: data, completion: { [weak self] in
self?.updateHighlightedContentNode()
completion()
})
}
}
private func updateHighlightedContentNode() {
guard let highlightedContentNode = self.highlightedContentNode, let highlightedColor = self.highlightedColor else {
return
}
if let contents = self.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID {
(highlightedContentNode.view as! UIImageView).image = UIImage(cgImage: contents as! CGImage).withRenderingMode(.alwaysTemplate)
}
highlightedContentNode.tintColor = highlightedColor
if self.highlightReplacesContent {
self.contents = nil
}
}
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
self.highlightReplacesContent = replace
var updated = false
if let current = self.highlightedColor, let color = color {
updated = !current.isEqual(color)
} else if (self.highlightedColor != nil) != (color != nil) {
updated = true
}
if !updated {
return
}
self.highlightedColor = color
if let _ = color {
if let highlightedContentNode = self.highlightedContentNode {
highlightedContentNode.alpha = 1.0
} else {
let highlightedContentNode = ASDisplayNode(viewBlock: {
return UIImageView()
}, didLoad: nil)
highlightedContentNode.displaysAsynchronously = false
self.highlightedContentNode = highlightedContentNode
highlightedContentNode.frame = self.bounds
self.addSubnode(highlightedContentNode)
}
self.updateHighlightedContentNode()
} else if let highlightedContentNode = self.highlightedContentNode {
highlightedContentNode.alpha = 0.0
highlightedContentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] completed in
guard let strongSelf = self, completed else {
return
}
strongSelf.highlightedContentNode?.removeFromSupernode()
strongSelf.highlightedContentNode = nil
})
}
}
}
@@ -0,0 +1,387 @@
import Foundation
import UIKit
import AsyncDisplayKit
import RLottieBinding
import SwiftSignalKit
import GZip
import Display
public final class DirectAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode {
private static let sharedQueue = Queue(name: "DirectAnimatedStickerNode", qos: .userInteractive)
private final class LoadFrameTask {
var isCancelled: Bool = false
}
public var automaticallyLoadFirstFrame: Bool = false
public var automaticallyLoadLastFrame: Bool = false
public var playToCompletionOnStop: Bool = false
private var didStart: Bool = false
public var started: () -> Void = {}
public var completed: (Bool) -> Void = { _ in }
private var didComplete: Bool = false
public var frameUpdated: (Int, Int) -> Void = { _, _ in }
public var currentFrameIndex: Int {
get {
return self.frameIndex
} set(value) {
}
}
public var currentFrameCount: Int {
get {
if let lottieInstance = self.lottieInstance {
return Int(lottieInstance.frameCount)
} else if let videoSource = self.videoSource {
return Int(videoSource.frameRate)
} else {
return 0
}
} set(value) {
}
}
public var currentFrameImage: UIImage? {
if let contents = self.layer.contents {
return UIImage(cgImage: contents as! CGImage)
} else {
return nil
}
}
public private(set) var isPlaying: Bool = false
public var stopAtNearestLoop: Bool = false
private let statusPromise = Promise<AnimatedStickerStatus>()
public var status: Signal<AnimatedStickerStatus, NoError> {
return self.statusPromise.get()
}
public var autoplay: Bool = true
public var visibility: Bool = false {
didSet {
self.updatePlayback()
}
}
public var overrideVisibility: Bool = false
public var isPlayingChanged: (Bool) -> Void = { _ in }
private var sourceDisposable: Disposable?
private var playbackSize: CGSize?
private var lottieInstance: LottieInstance?
private var videoSource: AnimatedStickerFrameSource?
private var frameIndex: Int = 0
private var playbackMode: AnimatedStickerPlaybackMode = .loop
private var frameImages: [Int: UIImage] = [:]
private var loadFrameTasks: [Int: LoadFrameTask] = [:]
private var nextFrameTimer: SwiftSignalKit.Timer?
override public init() {
super.init()
}
deinit {
self.sourceDisposable?.dispose()
self.nextFrameTimer?.invalidate()
}
public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) {
}
public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode) {
self.didStart = false
self.didComplete = false
self.sourceDisposable?.dispose()
self.playbackSize = CGSize(width: CGFloat(width), height: CGFloat(height))
self.playbackMode = playbackMode
self.sourceDisposable = (source.directDataPath(attemptSynchronously: false)
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] path in
guard let strongSelf = self, let path = path else {
return
}
if source.isVideo {
if let videoSource = makeVideoStickerDirectFrameSource(queue: DirectAnimatedStickerNode.sharedQueue, path: path, hintVP9: true, width: width, height: height, cachePathPrefix: nil, unpremultiplyAlpha: false) {
strongSelf.setupPlayback(videoSource: videoSource)
}
} else {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return
}
let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data
guard let lottieInstance = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
print("Could not load sticker data")
return
}
strongSelf.setupPlayback(lottieInstance: lottieInstance)
}
}).strict()
}
private func updatePlayback() {
let isPlaying = self.visibility && (self.lottieInstance != nil || self.videoSource != nil)
if self.isPlaying != isPlaying {
self.isPlaying = isPlaying
if self.isPlaying {
self.startNextFrameTimerIfNeeded()
self.updateLoadFrameTasks()
} else {
self.nextFrameTimer?.invalidate()
self.nextFrameTimer = nil
}
self.isPlayingChanged(self.isPlaying)
}
}
private func startNextFrameTimerIfNeeded() {
var frameRate: Double?
if let lottieInstance = self.lottieInstance {
frameRate = Double(lottieInstance.frameRate)
} else if let videoSource = self.videoSource {
frameRate = Double(videoSource.frameRate)
}
if self.nextFrameTimer == nil, let frameRate = frameRate, self.frameImages[self.frameIndex] != nil {
let nextFrameTimer = SwiftSignalKit.Timer(timeout: 1.0 / frameRate, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.nextFrameTimer = nil
strongSelf.advanceFrameIfPossible()
}, queue: .mainQueue())
self.nextFrameTimer = nextFrameTimer
nextFrameTimer.start()
}
}
private func advanceFrameIfPossible() {
var frameCount: Int?
if let lottieInstance = self.lottieInstance {
frameCount = Int(lottieInstance.frameCount)
} else if let videoSource = self.videoSource {
frameCount = Int(videoSource.frameCount)
}
guard let frameCount = frameCount else {
return
}
if self.frameIndex == frameCount - 1 {
switch self.playbackMode {
case .loop:
self.completed(false)
case let .count(count):
if count <= 1 {
if !self.didComplete {
self.didComplete = true
self.completed(true)
}
return
} else {
self.playbackMode = .count(count - 1)
self.completed(false)
}
case .once:
if !self.didComplete {
self.didComplete = true
self.completed(true)
}
return
case .still:
break
}
}
let nextFrameIndex = (self.frameIndex + 1) % frameCount
self.frameIndex = nextFrameIndex
self.updateFrameImageIfNeeded()
self.updateLoadFrameTasks()
}
private func updateFrameImageIfNeeded() {
var frameCount: Int?
if let lottieInstance = self.lottieInstance {
frameCount = Int(lottieInstance.frameCount)
} else if let videoSource = self.videoSource {
frameCount = Int(videoSource.frameCount)
}
guard let frameCount = frameCount else {
return
}
var allowedIndices: [Int] = []
for i in 0 ..< 2 {
let mappedIndex = (self.frameIndex + i) % frameCount
allowedIndices.append(mappedIndex)
}
var removeKeys: [Int] = []
for index in self.frameImages.keys {
if !allowedIndices.contains(index) {
removeKeys.append(index)
}
}
for index in removeKeys {
self.frameImages.removeValue(forKey: index)
}
for (index, task) in self.loadFrameTasks {
if !allowedIndices.contains(index) {
task.isCancelled = true
}
}
if let image = self.frameImages[self.frameIndex] {
self.layer.contents = image.cgImage
self.frameUpdated(self.frameIndex, frameCount)
if !self.didComplete {
self.startNextFrameTimerIfNeeded()
}
if !self.didStart {
self.didStart = true
self.started()
}
}
}
private func updateLoadFrameTasks() {
var frameCount: Int?
if let lottieInstance = self.lottieInstance {
frameCount = Int(lottieInstance.frameCount)
} else if let videoSource = self.videoSource {
frameCount = Int(videoSource.frameCount)
}
guard let frameCount = frameCount else {
return
}
let frameIndex = self.frameIndex % frameCount
if self.frameImages[frameIndex] == nil {
self.maybeStartLoadFrameTask(frameIndex: frameIndex)
} else {
self.maybeStartLoadFrameTask(frameIndex: (frameIndex + 1) % frameCount)
}
}
private func maybeStartLoadFrameTask(frameIndex: Int) {
guard self.lottieInstance != nil || self.videoSource != nil else {
return
}
guard let playbackSize = self.playbackSize else {
return
}
if self.loadFrameTasks[frameIndex] != nil {
return
}
let task = LoadFrameTask()
self.loadFrameTasks[frameIndex] = task
let lottieInstance = self.lottieInstance
let videoSource = self.videoSource
DirectAnimatedStickerNode.sharedQueue.async { [weak self] in
var image: UIImage?
if !task.isCancelled {
if let lottieInstance = lottieInstance {
if let drawingContext = DrawingContext(size: playbackSize, scale: 1.0, opaque: false, clear: false) {
lottieInstance.renderFrame(with: Int32(frameIndex), into: drawingContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(drawingContext.scaledSize.width), height: Int32(drawingContext.scaledSize.height), bytesPerRow: Int32(drawingContext.bytesPerRow))
image = drawingContext.generateImage()
}
} else if let videoSource = videoSource {
if let frame = videoSource.takeFrame(draw: true) {
if let drawingContext = DrawingContext(size: CGSize(width: frame.width, height: frame.height), scale: 1.0, opaque: false, clear: false, bytesPerRow: frame.bytesPerRow) {
frame.data.copyBytes(to: drawingContext.bytes.assumingMemoryBound(to: UInt8.self), from: 0 ..< min(frame.data.count, drawingContext.length))
image = drawingContext.generateImage()
}
}
}
}
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if let currentTask = strongSelf.loadFrameTasks[frameIndex], currentTask === task {
strongSelf.loadFrameTasks.removeValue(forKey: frameIndex)
}
if !task.isCancelled, let image = image {
strongSelf.frameImages[frameIndex] = image
strongSelf.updateFrameImageIfNeeded()
strongSelf.updateLoadFrameTasks()
}
}
}
}
private func setupPlayback(lottieInstance: LottieInstance) {
self.lottieInstance = lottieInstance
self.updatePlayback()
}
private func setupPlayback(videoSource: AnimatedStickerFrameSource) {
self.videoSource = videoSource
self.updatePlayback()
}
public func reset() {
}
public func playOnce() {
}
public func playLoop() {
}
public func play(firstFrame: Bool, fromIndex: Int?) {
if let fromIndex = fromIndex {
self.frameIndex = fromIndex
self.updateLoadFrameTasks()
}
}
public func pause() {
}
public func stop() {
}
public func seekTo(_ position: AnimatedStickerPlaybackPosition) {
}
public func playIfNeeded() -> Bool {
return false
}
public func updateLayout(size: CGSize) {
}
public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
}
}
@@ -0,0 +1,80 @@
import Foundation
import Compression
import YuvConversion
func compressFrame(width: Int, height: Int, rgbData: Data, unpremultiply: Bool) -> Data? {
let bytesPerRow = rgbData.count / height
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
assert(yuvaPixelsPerAlphaRow % 2 == 0)
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
let yuvaFrameData = malloc(yuvaLength)!
defer {
free(yuvaFrameData)
}
memset(yuvaFrameData, 0, yuvaLength)
var compressedFrameData = Data(count: yuvaLength)
let compressedFrameDataLength = compressedFrameData.count
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
defer {
free(scratchData)
}
var rgbData = rgbData
rgbData.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) -> Void in
if let baseAddress = buffer.baseAddress {
encodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), baseAddress.assumingMemoryBound(to: UInt8.self), Int32(width), Int32(height), Int32(bytesPerRow), true, unpremultiply)
}
}
var maybeResultSize: Int?
compressedFrameData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let length = compression_encode_buffer(bytes, compressedFrameDataLength, yuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
maybeResultSize = length
}
guard let resultSize = maybeResultSize else {
return nil
}
compressedFrameData.count = resultSize
return compressedFrameData
}
func compressFrame(width: Int, height: Int, yuvaData: Data) -> Data? {
let yuvaLength = yuvaData.count
var compressedFrameData = Data(count: yuvaLength)
let compressedFrameDataLength = compressedFrameData.count
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
defer {
free(scratchData)
}
var maybeResultSize: Int?
yuvaData.withUnsafeBytes { yuvaBuffer -> Void in
if let yuvaFrameData = yuvaBuffer.baseAddress {
compressedFrameData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let length = compression_encode_buffer(bytes, compressedFrameDataLength, yuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
maybeResultSize = length
}
}
}
guard let resultSize = maybeResultSize else {
return nil
}
compressedFrameData.count = resultSize
return compressedFrameData
}
@@ -0,0 +1,260 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import YuvConversion
import Accelerate
final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
private let templateImageSupport: Bool
private var highlightedContentNode: ASDisplayNode?
private var highlightedColor: UIColor?
private var highlightReplacesContent = false
public var renderAsTemplateImage: Bool = false
public private(set) var currentFrameImage: UIImage?
init(templateImageSupport: Bool) {
self.templateImageSupport = templateImageSupport
super.init()
if templateImageSupport {
self.setViewBlock({
return UIImageView()
})
}
}
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?) {
assert(bytesPerRow > 0)
let renderAsTemplateImage = self.renderAsTemplateImage
queue.async { [weak self] in
switch type {
case .argb:
let calculatedBytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(width))
assert(bytesPerRow == calculatedBytesPerRow)
case .yuva:
break
case .dct:
break
}
var image: UIImage?
var averageColorValue: UIColor?
autoreleasepool {
image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, contextBytesPerRow in
switch type {
case .yuva:
data.withUnsafeBytes { bytes -> Void in
guard let baseAddress = bytes.baseAddress else {
return
}
if bytesPerRow <= 0 || height <= 0 || width <= 0 || bytesPerRow * height > bytes.count {
assert(false)
return
}
decodeYUVAToRGBA(baseAddress.assumingMemoryBound(to: UInt8.self), pixelData, Int32(width), Int32(height), Int32(contextBytesPerRow))
}
case .argb:
var data = data
data.withUnsafeMutableBytes { bytes -> Void in
guard let baseAddress = bytes.baseAddress else {
return
}
if mulAlpha {
var srcData = vImage_Buffer(data: baseAddress.assumingMemoryBound(to: UInt8.self), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow)
var destData = vImage_Buffer(data: pixelData, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow)
let permuteMap: [UInt8] = [3, 2, 1, 0]
vImagePermuteChannels_ARGB8888(&srcData, &destData, permuteMap, vImage_Flags(kvImageDoNotTile))
vImagePremultiplyData_ARGB8888(&destData, &destData, vImage_Flags(kvImageDoNotTile))
vImagePermuteChannels_ARGB8888(&destData, &destData, permuteMap, vImage_Flags(kvImageDoNotTile))
} else {
memcpy(pixelData, baseAddress.assumingMemoryBound(to: UInt8.self), bytes.count)
}
}
case .dct:
break
}
})
if renderAsTemplateImage {
image = image?.withRenderingMode(.alwaysTemplate)
}
if averageColor != nil {
let blurredWidth = 16
let blurredHeight = 16
let blurredBytesPerRow = blurredWidth * 4
guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else {
return
}
let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight))
if let image, let cgImage = image.cgImage {
context.withFlippedContext { c in
c.setFillColor(UIColor.white.cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8)))
}
}
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(blurredWidth)
destinationBuffer.height = UInt(blurredHeight)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = context.bytesPerRow
vImageBoxConvolve_ARGB8888(&destinationBuffer,
&destinationBuffer,
nil,
0, 0,
UInt32(15),
UInt32(15),
nil,
vImage_Flags(kvImageTruncateKernel))
let divisor: Int32 = 0x1000
let rwgt: CGFloat = 0.3086
let gwgt: CGFloat = 0.6094
let bwgt: CGFloat = 0.0820
let adjustSaturation: CGFloat = 1.7
let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation
let b = (1.0 - adjustSaturation) * rwgt
let c = (1.0 - adjustSaturation) * rwgt
let d = (1.0 - adjustSaturation) * gwgt
let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation
let f = (1.0 - adjustSaturation) * gwgt
let g = (1.0 - adjustSaturation) * bwgt
let h = (1.0 - adjustSaturation) * bwgt
let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation
let satMatrix: [CGFloat] = [
a, b, c, 0,
d, e, f, 0,
g, h, i, 0,
0, 0, 0, 1
]
var matrix: [Int16] = satMatrix.map { value in
return Int16(value * CGFloat(divisor))
}
vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
context.withFlippedContext { c in
c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
}
var sumR: UInt64 = 0
var sumG: UInt64 = 0
var sumB: UInt64 = 0
var sumA: UInt64 = 0
for y in 0 ..< blurredHeight {
let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow)
for x in 0 ..< blurredWidth {
let pixel = row.advanced(by: x * 4)
sumB += UInt64(pixel.advanced(by: 0).pointee)
sumG += UInt64(pixel.advanced(by: 1).pointee)
sumR += UInt64(pixel.advanced(by: 2).pointee)
sumA += UInt64(pixel.advanced(by: 3).pointee)
}
}
sumR /= UInt64(blurredWidth * blurredHeight)
sumG /= UInt64(blurredWidth * blurredHeight)
sumB /= UInt64(blurredWidth * blurredHeight)
sumA /= UInt64(blurredWidth * blurredHeight)
sumA = 255
averageColorValue = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0)
}
}
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.currentFrameImage = image
if strongSelf.templateImageSupport {
(strongSelf.view as? UIImageView)?.image = image
} else {
strongSelf.contents = image?.cgImage
}
strongSelf.updateHighlightedContentNode()
if strongSelf.highlightedContentNode?.frame != strongSelf.bounds {
strongSelf.highlightedContentNode?.frame = strongSelf.bounds
}
completion()
if let averageColor, let averageColorValue {
averageColor(averageColorValue)
}
}
}
}
private func updateHighlightedContentNode() {
guard let highlightedContentNode = self.highlightedContentNode, let highlightedColor = self.highlightedColor else {
return
}
(highlightedContentNode.view as! UIImageView).image = self.currentFrameImage?.withRenderingMode(.alwaysTemplate)
highlightedContentNode.tintColor = highlightedColor
if self.highlightReplacesContent {
if self.templateImageSupport {
(self.view as? UIImageView)?.image = nil
} else {
self.contents = nil
}
}
}
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
self.highlightReplacesContent = replace
var updated = false
if let current = self.highlightedColor, let color = color {
updated = !current.isEqual(color)
} else if (self.highlightedColor != nil) != (color != nil) {
updated = true
}
if !updated {
return
}
self.highlightedColor = color
if let _ = color {
if let highlightedContentNode = self.highlightedContentNode {
highlightedContentNode.alpha = 1.0
} else {
let highlightedContentNode = ASDisplayNode(viewBlock: {
return UIImageView()
}, didLoad: nil)
highlightedContentNode.displaysAsynchronously = false
self.highlightedContentNode = highlightedContentNode
highlightedContentNode.frame = self.bounds
self.addSubnode(highlightedContentNode)
}
self.updateHighlightedContentNode()
} else if let highlightedContentNode = self.highlightedContentNode {
highlightedContentNode.alpha = 0.0
highlightedContentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] completed in
guard let strongSelf = self, completed else {
return
}
strongSelf.highlightedContentNode?.removeFromSupernode()
strongSelf.highlightedContentNode = nil
})
}
}
}
@@ -0,0 +1,457 @@
import Foundation
import Compression
import Display
import SwiftSignalKit
import UniversalMediaPlayer
import CoreMedia
import ManagedFile
import Accelerate
import TelegramCore
import WebPBinding
import UIKit
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
private let maximumFrameCount = 30 * 10
private final class VideoStickerFrameSourceCache {
private enum FrameRangeResult {
case range(Range<Int>)
case notFound
case corruptedFile
}
private let queue: Queue
private let storeQueue: Queue
private let path: String
private let file: ManagedFile
private let width: Int
private let height: Int
public private(set) var frameRate: Int32 = 0
public private(set) var frameCount: Int32 = 0
private var isStoringFrames = Set<Int>()
var storedFrames: Int {
return self.isStoringFrames.count
}
private var scratchBuffer: Data
private var decodeBuffer: Data
init?(queue: Queue, pathPrefix: String, width: Int, height: Int) {
self.queue = queue
self.storeQueue = sharedStoreQueue
self.width = width
self.height = height
let version: Int = 3
self.path = "\(pathPrefix)_\(width)x\(height)-v\(version).vstickerframecache"
var file = ManagedFile(queue: queue, path: self.path, mode: .readwrite)
if let file = file {
self.file = file
} else {
let _ = try? FileManager.default.removeItem(atPath: self.path)
file = ManagedFile(queue: queue, path: self.path, mode: .readwrite)
if let file = file {
self.file = file
} else {
return nil
}
}
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
self.decodeBuffer = Data(count: yuvaLength)
self.initializeFrameTable()
}
deinit {
if self.frameCount == 0 {
let _ = try? FileManager.default.removeItem(atPath: self.path)
}
}
private func initializeFrameTable() {
var reset = true
if let size = self.file.getSize(), size >= maximumFrameCount {
if self.readFrameRate() {
reset = false
}
}
if reset {
self.file.truncate(count: 0)
var zero: Int32 = 0
let _ = self.file.write(&zero, count: 4)
let _ = self.file.write(&zero, count: 4)
for _ in 0 ..< maximumFrameCount {
let _ = self.file.write(&zero, count: 4)
let _ = self.file.write(&zero, count: 4)
}
}
}
private func readFrameRate() -> Bool {
guard self.frameCount == 0 else {
return true
}
let _ = self.file.seek(position: 0)
var frameRate: Int32 = 0
if self.file.read(&frameRate, 4) != 4 {
return false
}
if frameRate < 0 {
return false
}
if frameRate == 0 {
return false
}
self.frameRate = frameRate
let _ = self.file.seek(position: 4)
var frameCount: Int32 = 0
if self.file.read(&frameCount, 4) != 4 {
return false
}
if frameCount < 0 {
return false
}
if frameCount == 0 {
return false
}
self.frameCount = frameCount
return true
}
private func readFrameRange(index: Int) -> FrameRangeResult {
if index < 0 || index >= maximumFrameCount {
return .notFound
}
guard self.readFrameRate() else {
return .notFound
}
if index >= self.frameCount {
return .notFound
}
let _ = self.file.seek(position: Int64(8 + index * 4 * 2))
var offset: Int32 = 0
var length: Int32 = 0
if self.file.read(&offset, 4) != 4 {
return .corruptedFile
}
if self.file.read(&length, 4) != 4 {
return .corruptedFile
}
if length == 0 {
return .notFound
}
if length < 0 || offset < 0 {
return .corruptedFile
}
if Int64(offset) + Int64(length) > 100 * 1024 * 1024 {
return .corruptedFile
}
return .range(Int(offset) ..< Int(offset + length))
}
func storeFrameRateAndCount(frameRate: Int, frameCount: Int) {
let _ = self.file.seek(position: 0)
var frameRate = Int32(frameRate)
let _ = self.file.write(&frameRate, count: 4)
let _ = self.file.seek(position: 4)
var frameCount = Int32(frameCount)
let _ = self.file.write(&frameCount, count: 4)
}
func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
if index < 0 || index >= maximumFrameCount {
return
}
if self.isStoringFrames.contains(index) {
return
}
self.isStoringFrames.insert(index)
let width = self.width
let height = self.height
let queue = self.queue
self.storeQueue.async { [weak self] in
let compressedData = compressFrame(width: width, height: height, rgbData: rgbData, unpremultiply: false)
queue.async {
guard let strongSelf = self else {
return
}
guard let currentSize = strongSelf.file.getSize() else {
return
}
guard let compressedData = compressedData else {
return
}
let _ = strongSelf.file.seek(position: Int64(8 + index * 4 * 2))
var offset = Int32(currentSize)
var length = Int32(compressedData.count)
let _ = strongSelf.file.write(&offset, count: 4)
let _ = strongSelf.file.write(&length, count: 4)
let _ = strongSelf.file.seek(position: Int64(currentSize))
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
if let baseAddress = buffer.baseAddress {
let _ = strongSelf.file.write(baseAddress, count: Int(length))
}
}
}
}
}
func readUncompressedYuvaFrame(index: Int) -> Data? {
if index < 0 || index >= maximumFrameCount {
return nil
}
let rangeResult = self.readFrameRange(index: index)
switch rangeResult {
case let .range(range):
let _ = self.file.seek(position: Int64(range.lowerBound))
let length = range.upperBound - range.lowerBound
let compressedData = self.file.readData(count: length)
if compressedData.count != length {
return nil
}
var frameData: Data?
let decodeBufferLength = self.decodeBuffer.count
compressedData.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.scratchBuffer.withUnsafeMutableBytes { scratchBuffer -> Void in
guard let scratchBytes = scratchBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
self.decodeBuffer.withUnsafeMutableBytes { decodeBuffer -> Void in
guard let decodeBytes = decodeBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
frameData = Data(bytes: decodeBytes, count: resultLength)
}
}
}
return frameData
case .notFound:
return nil
case .corruptedFile:
self.file.truncate(count: 0)
self.initializeFrameTable()
return nil
}
}
}
private let useCache = true
public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, hintVP9: Bool, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool) -> AnimatedStickerFrameSource? {
return VideoStickerDirectFrameSource(queue: queue, path: path, isVP9: hintVP9, width: width, height: height, cachePathPrefix: cachePathPrefix, unpremultiplyAlpha: unpremultiplyAlpha)
}
public final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource {
private let queue: Queue
private let path: String
private let width: Int
private let height: Int
private let cache: VideoStickerFrameSourceCache?
private let image: UIImage?
private let bytesPerRow: Int
public var frameCount: Int
public let frameRate: Int
public var duration: Double
fileprivate var currentFrame: Int
private var source: FFMpegFileReader?
public var frameIndex: Int {
if self.frameCount == 0 {
return 0
} else {
return self.currentFrame % self.frameCount
}
}
public init?(queue: Queue, path: String, isVP9: Bool = true, width: Int, height: Int, cachePathPrefix: String?, unpremultiplyAlpha: Bool = true) {
self.queue = queue
self.path = path
self.width = width
self.height = height
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(self.width))
self.currentFrame = 0
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
VideoStickerFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height)
}
if useCache, let cache = self.cache, cache.frameCount > 0 {
self.source = nil
self.image = nil
self.frameRate = Int(cache.frameRate)
self.frameCount = Int(cache.frameCount)
if self.frameRate > 0 {
self.duration = Double(self.frameCount) / Double(self.frameRate)
} else {
self.duration = 0.0
}
} else if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let image = WebP.convert(fromWebP: data) {
self.source = nil
self.image = image
self.frameRate = 1
self.frameCount = 1
self.duration = 0.0
} else {
let source = FFMpegFileReader(
source: .file(path),
passthroughDecoder: false,
useHardwareAcceleration: false,
selectedStream: .mediaType(.video),
seek: nil,
maxReadablePts: nil
)
if let source {
self.source = source
self.frameRate = min(30, source.frameRate())
self.duration = source.duration().seconds
} else {
self.source = nil
self.frameRate = 30
self.duration = 0.0
}
self.image = nil
self.frameCount = 0
}
}
deinit {
assert(self.queue.isCurrent())
}
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
let frameIndex: Int
if self.frameCount > 0 {
frameIndex = self.currentFrame % self.frameCount
} else {
frameIndex = self.currentFrame
}
self.currentFrame += 1
if draw {
if let image = self.image {
guard let context = DrawingContext(size: CGSize(width: self.width, height: self.height), scale: 1.0, opaque: false, clear: true, bytesPerRow: self.bytesPerRow) else {
return nil
}
context.withFlippedContext { c in
c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: context.size))
}
let frameData = Data(bytes: context.bytes, count: self.bytesPerRow * self.height)
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount, multiplyAlpha: true)
} else if useCache, let cache = self.cache, let yuvData = cache.readUncompressedYuvaFrame(index: frameIndex) {
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
} else if let source = self.source {
let frameAndLoop = source.readFrame(argb: true)
switch frameAndLoop {
case let .frame(frame):
var frameData = Data(count: self.bytesPerRow * self.height)
frameData.withUnsafeMutableBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
let imageBuffer = CMSampleBufferGetImageBuffer(frame.sampleBuffer)
CVPixelBufferLockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer!)
let width = CVPixelBufferGetWidth(imageBuffer!)
let height = CVPixelBufferGetHeight(imageBuffer!)
let srcData = CVPixelBufferGetBaseAddress(imageBuffer!)
var sourceBuffer = vImage_Buffer(data: srcData, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow)
var destBuffer = vImage_Buffer(data: bytes, height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: self.bytesPerRow)
let _ = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, vImage_Flags(kvImageDoNotTile))
CVPixelBufferUnlockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
}
self.cache?.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount, multiplyAlpha: true)
case .endOfStream:
if self.frameCount == 0 {
if let cache = self.cache {
if cache.storedFrames == frameIndex {
self.frameCount = frameIndex
cache.storeFrameRateAndCount(frameRate: self.frameRate, frameCount: self.frameCount)
} else {
Logger.shared.log("VideoSticker", "Missed a frame? \(frameIndex) \(cache.storedFrames)")
}
} else {
self.frameCount = frameIndex
}
}
self.currentFrame = 0
self.source = FFMpegFileReader(
source: .file(self.path),
passthroughDecoder: false,
useHardwareAcceleration: false,
selectedStream: .mediaType(.video),
seek: nil,
maxReadablePts: nil
)
if let cache = self.cache {
if let yuvData = cache.readUncompressedYuvaFrame(index: self.currentFrame) {
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.width * 2, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount)
}
}
return nil
case .waitingForMoreData, .error:
return nil
}
} else {
return nil
}
} else {
return nil
}
}
public func skipToEnd() {
self.currentFrame = self.frameCount - 1
}
public func skipToFrameIndex(_ index: Int) {
self.currentFrame = index
}
}
+88
View File
@@ -0,0 +1,88 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "AnimationCompressionMetalResources",
srcs = glob([
"Resources/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "AnimationCompressionBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.AnimationCompression</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>AnimationCompression</string>
"""
)
apple_resource_bundle(
name = "AnimationCompressionBundle",
infoplists = [
":AnimationCompressionBundleInfoPlist",
],
resources = [
":AnimationCompressionMetalResources",
],
)
swift_library(
name = "AnimationCompression",
module_name = "AnimationCompression",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":AnimationCompressionBundle",
],
deps = [
":DctHuffman",
"//submodules/Components/MetalImageView:MetalImageView",
],
visibility = [
"//visibility:public",
],
)
objc_library(
name = "DctHuffman",
enable_modules = True,
module_name = "DctHuffman",
srcs = glob([
"DctHuffman/Sources/**/*.m",
"DctHuffman/Sources/**/*.mm",
"DctHuffman/Sources/**/*.h",
], allow_empty=True),
copts = [],
hdrs = glob([
"DctHuffman/PublicHeaders/**/*.h",
]),
includes = [
"DctHuffman/PublicHeaders",
],
deps = [
],
sdk_frameworks = [
"Foundation",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,17 @@
#ifndef DctHuffman_h
#define DctHuffman_h
#import <Foundation/Foundation.h>
#ifdef __cplusplus
extern "C" {
#endif
NSData * _Nullable writeDCTBlocks(int width, int height, float const * _Nonnull coefficients);
void readDCTBlocks(int width, int height, NSData * _Nonnull blockData, float * _Nonnull coefficients, int elementsPerRow);
#ifdef __cplusplus
}
#endif
#endif /* DctHuffman_h */
@@ -0,0 +1,630 @@
#import <DctHuffman/DctHuffman.h>
#include <functional>
#include <vector>
namespace DctHuffman {
typedef std::function<void(unsigned char)> WRITE_ONE_BYTE;
}
namespace
{
using uint8_t = unsigned char;
using uint16_t = unsigned short;
using int16_t = short;
using int32_t = int;
const uint8_t ZigZagInv[8*8] = {
0, 1, 8,16, 9, 2, 3, 10,
17,24,32,25,18,11, 4, 5,
12,19,26,33,40,48,41,34,
27,20,13, 6, 7,14,21,28,
35,42,49,56,57,50,43,36,
29,22,15,23,30,37,44,51,
58,59,52,45,38,31,39,46,
53,60,61,54,47,55,62,63
};
const uint8_t ZigZag[] = {
0, 1, 5, 6,14,15,27,28,
2, 4, 7,13,16,26,29,42,
3, 8,12,17,25,30,41,43,
9,11,18,24,31,40,44,53,
10,19,23,32,39,45,52,54,
20,22,33,38,46,51,55,60,
21,34,37,47,50,56,59,61,
35,36,48,49,57,58,62,63
};
// Huffman definitions for first DC/AC tables (luminance / Y channel)
const uint8_t DcLuminanceCodesPerBitsize[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; // sum = 12
const uint8_t DcLuminanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes
const uint8_t AcLuminanceCodesPerBitsize[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 }; // sum = 162
const uint8_t AcLuminanceValues [162] = // => 162 codes
{ 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, // 16*10+2 symbols because
0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, // upper 4 bits can be 0..F
0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, // while lower 4 bits can be 1..A
0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, // plus two special codes 0x00 and 0xF0
0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, // order of these symbols was determined empirically by JPEG committee
0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2,
0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
// Huffman definitions for second DC/AC tables (chrominance / Cb and Cr channels)
const uint8_t DcChrominanceCodesPerBitsize[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; // sum = 12
const uint8_t DcChrominanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes (identical to DcLuminanceValues)
const uint8_t AcChrominanceCodesPerBitsize[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 }; // sum = 162
const uint8_t AcChrominanceValues [162] = // => 162 codes
{ 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, // same number of symbol, just different order
0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, // (which is more efficient for AC coding)
0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,
0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87,
0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,
0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,
0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
const int16_t CodeWordLimit = 2048; // +/-2^11, maximum value after DCT
// represent a single Huffman code
struct BitCode {
BitCode() = default; // undefined state, must be initialized at a later time
BitCode(uint16_t code_, uint8_t numBits_)
: code(code_), numBits(numBits_) {}
uint16_t code; // JPEG's Huffman codes are limited to 16 bits
uint8_t numBits; // number of valid bits
};
// wrapper for bit output operations
struct BitWriter {
// user-supplied callback that writes/stores one byte
DctHuffman::WRITE_ONE_BYTE output;
// initialize writer
explicit BitWriter(DctHuffman::WRITE_ONE_BYTE output_) : output(output_) {}
// store the most recently encoded bits that are not written yet
struct BitBuffer
{
int32_t data = 0; // actually only at most 24 bits are used
uint8_t numBits = 0; // number of valid bits (the right-most bits)
} buffer;
// write Huffman bits stored in BitCode, keep excess bits in BitBuffer
BitWriter& operator<<(const BitCode& data)
{
// append the new bits to those bits leftover from previous call(s)
buffer.numBits += data.numBits;
buffer.data <<= data.numBits;
buffer.data |= data.code;
// write all "full" bytes
while (buffer.numBits >= 8)
{
// extract highest 8 bits
buffer.numBits -= 8;
auto oneByte = uint8_t(buffer.data >> buffer.numBits);
output(oneByte);
if (oneByte == 0xFF) // 0xFF has a special meaning for JPEGs (it's a block marker)
output(0); // therefore pad a zero to indicate "nope, this one ain't a marker, it's just a coincidence"
// note: I don't clear those written bits, therefore buffer.bits may contain garbage in the high bits
// if you really want to "clean up" (e.g. for debugging purposes) then uncomment the following line
//buffer.bits &= (1 << buffer.numBits) - 1;
}
return *this;
}
// write all non-yet-written bits, fill gaps with 1s (that's a strange JPEG thing)
void flush()
{
// at most seven set bits needed to "fill" the last byte: 0x7F = binary 0111 1111
*this << BitCode(0x7F, 7); // I should set buffer.numBits = 0 but since there are no single bits written after flush() I can safely ignore it
}
// NOTE: all the following BitWriter functions IGNORE the BitBuffer and write straight to output !
// write a single byte
BitWriter& operator<<(uint8_t oneByte)
{
output(oneByte);
return *this;
}
// write an array of bytes
template <typename T, int Size>
BitWriter& operator<<(T (&manyBytes)[Size])
{
for (auto c : manyBytes)
output(c);
return *this;
}
// start a new JFIF block
void addMarker(uint8_t id, uint16_t length)
{
output(0xFF); output(id); // ID, always preceded by 0xFF
output(uint8_t(length >> 8)); // length of the block (big-endian, includes the 2 length bytes as well)
output(uint8_t(length & 0xFF));
}
};
// ////////////////////////////////////////
// functions / templates
// same as std::min()
template <typename Number>
Number minimum(Number value, Number maximum)
{
return value <= maximum ? value : maximum;
}
// restrict a value to the interval [minimum, maximum]
template <typename Number, typename Limit>
Number clamp(Number value, Limit minValue, Limit maxValue)
{
if (value <= minValue) return minValue; // never smaller than the minimum
if (value >= maxValue) return maxValue; // never bigger than the maximum
return value; // value was inside interval, keep it
}
int16_t encodeDCTBlock(BitWriter& writer, float block64[64], int16_t lastDC,
const BitCode huffmanDC[256], const BitCode huffmanAC[256], const BitCode* codewords) {
// encode DC (the first coefficient is the "average color" of the 8x8 block)
auto DC = int(block64[0] + (block64[0] >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
// quantize and zigzag the other 63 coefficients
auto posNonZero = 0; // find last coefficient which is not zero (because trailing zeros are encoded differently)
int16_t quantized[8*8];
for (auto i = 1; i < 8*8; i++) // start at 1 because block64[0]=DC was already processed
{
auto value = block64[ZigZagInv[i]];
// round to nearest integer
quantized[i] = int(value + (value >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
// remember offset of last non-zero coefficient
if (quantized[i] != 0)
posNonZero = i;
}
// same "average color" as previous block ?
auto diff = DC - lastDC;
if (diff == 0)
writer << huffmanDC[0x00]; // yes, write a special short symbol
else
{
auto bits = codewords[diff]; // nope, encode the difference to previous block's average color
writer << huffmanDC[bits.numBits] << bits;
}
// encode ACs (quantized[1..63])
auto offset = 0; // upper 4 bits count the number of consecutive zeros
for (auto i = 1; i <= posNonZero; i++) // quantized[0] was already written, skip all trailing zeros, too
{
// zeros are encoded in a special way
while (quantized[i] == 0) // found another zero ?
{
offset += 0x10; // add 1 to the upper 4 bits
// split into blocks of at most 16 consecutive zeros
if (offset > 0xF0) // remember, the counter is in the upper 4 bits, 0xF = 15
{
writer << huffmanAC[0xF0]; // 0xF0 is a special code for "16 zeros"
offset = 0;
}
i++;
}
auto encoded = codewords[quantized[i]];
// combine number of zeros with the number of bits of the next non-zero value
writer << huffmanAC[offset + encoded.numBits] << encoded; // and the value itself
offset = 0;
}
// send end-of-block code (0x00), only needed if there are trailing zeros
if (posNonZero < 8*8 - 1) // = 63
writer << huffmanAC[0x00];
return DC;
}
// Jon's code includes the pre-generated Huffman codes
// I don't like these "magic constants" and compute them on my own :-)
void generateHuffmanTable(const uint8_t numCodes[16], const uint8_t* values, BitCode result[256])
{
// process all bitsizes 1 thru 16, no JPEG Huffman code is allowed to exceed 16 bits
auto huffmanCode = 0;
for (auto numBits = 1; numBits <= 16; numBits++)
{
// ... and each code of these bitsizes
for (auto i = 0; i < numCodes[numBits - 1]; i++) // note: numCodes array starts at zero, but smallest bitsize is 1
result[*values++] = BitCode(huffmanCode++, numBits);
// next Huffman code needs to be one bit wider
huffmanCode <<= 1;
}
}
} // end of anonymous namespace
// -------------------- externally visible code --------------------
namespace DctHuffman {
bool readMoreData(std::vector<uint8_t> const &bytes, int &readPosition, unsigned int &data, unsigned int &currentDataLength) {
unsigned char binaryData;
// Detect errors
if (currentDataLength > 24) { // Unsigned int can hold at most 32 = 24+8 bits
//cout << "ERROR: Code value not found in Huffman table: "<<data<<endl;
// Truncate data one by one bit in hope that we will eventually find a correct code
data = data - ((data >> (currentDataLength-1)) << (currentDataLength-1));
currentDataLength--;
return true;
}
if (readPosition + 1 >= bytes.size()) {
return false;
}
binaryData = bytes[readPosition];
readPosition++;
// We read byte and put it in low 8 bits of variable data
if (binaryData == 0xFF) {
data = (data << 8) + binaryData;
currentDataLength += 8; // Increase current data length for 8 because we read one new byte
if (readPosition + 1 >= bytes.size()) {
return false;
}
binaryData = bytes[readPosition];
readPosition++;
// End of Image marker
if (binaryData == 0xd9) {
// Drop 0xFF from data
data = data >> 8;
currentDataLength -= 8;
#if DEBUGLEVEL>1
cout << "End of image marker"<<endl;
#endif
return false;
}
// Restart marker means data goes blank
if (binaryData >= 0xd0 && binaryData <= 0xd7) {
/*#if DEBUGLEVEL>1
cout << "Restart marker"<<endl;
#endif*/
data = 0;
currentDataLength = 0;
/*for (uint i=0; i < components.size(); i++)
previousDC[i]=0;*/
}
// If after FF byte comes 0x00 byte, we ignore it, 0xFF is part of data (byte stuffing)
else if (binaryData != 0) {
data = (data << 8) + binaryData;
currentDataLength += 8; //Increase current data length for 8 because we read one new byte
#if DEBUGLEVEL>1
cout << "Stuffing"<<endl;
#endif
}
}
else {
data = (data << 8) + binaryData;
currentDataLength += 8;
}
return true;
}
bool readHuffmanBlock(std::vector<uint8_t> const &bytes, int &readPosition, int *dataBlock, unsigned int &data, unsigned int &currentDataLength, int currentComponent, BitCode const *componentTablesDC, BitCode const *componentTablesAC, int &previousDC) {
// Debugging
static unsigned int byteno = 0;
// Description of the 8x8 block currently being read
enum { AC, DC } ACDC = DC;
// How many AC elements should we read?
int ACcount = 64 - 1;
int m = 0; // Index into dataBlock
// Fill block with zeros
memset ((char*)dataBlock, 0, sizeof(int)*64);
bool endOfFile = false;
// Main loop
do {
// 3 bits is too small for a code
if (currentDataLength<3) {
continue;
}
// Some stats
byteno++;
// Current Huffman table
BitCode const *htable = componentTablesDC;
if (ACDC == AC) {
htable = componentTablesAC;
}
// Every one of 256 elements of the current Huffman table potentially has value, so we must go through all of them
for (int i = 0; i < 256; i++) {
// If code for i-th element is -1, then there is no Huffman code for i-th element
if (htable[i].numBits == 0) {
continue;
}
// If current data length is greater or equal than n, compare first n bits (n - length of current Huffman code)
uint n = htable[i].numBits;
if (currentDataLength < n) {
continue;
}
if (currentDataLength >= n && htable[i].code == data >> (currentDataLength - n)) {
// Remove first n bits from data;
currentDataLength -= n;
data = data - (htable[i].code << currentDataLength);
// Reading of DC coefficients
if (ACDC == DC) {
unsigned char bitLength = i; // Next i bits represent DC coefficient value
// Do we need to read more bits of data?
while (currentDataLength<bitLength) {
if (!readMoreData(bytes, readPosition, data, currentDataLength)) {
endOfFile = true;
break;
}
byteno++;
}
// Read out DC coefficient
int DCCoeficient = data >> (currentDataLength-bitLength);
currentDataLength -= bitLength;
data = data - (DCCoeficient << currentDataLength);
// If MSB in DC coefficient starts with 0, then substract value of DC with 2^bitlength+1
//cout << "Before substract "<<DCCoeficient<<" BL "<<int(bitLength)<<endl;
if ( bitLength != 0 && (DCCoeficient>>(bitLength-1)) == 0 ) {
DCCoeficient = DCCoeficient - (2 << (bitLength-1)) + 1;
}
//cout << "After substract "<<DCCoeficient<<" previousDC "<<previousDC[currentComponent]<<endl;
previousDC = DCCoeficient + previousDC;
dataBlock[m] = previousDC;
m++;
// No AC coefficients required?
if (ACcount == 0) {
return endOfFile;
}
// We generated our DC coefficient, next one is AC coefficient
ACDC = AC;
if (currentDataLength < 3) // If currentData length is < than 3, we need to read new byte, so leave this for loop
break;
i = -1; // CurrentDataLength is not zero, set i=0 to start from first element of array
htable = componentTablesAC;
} else {
// Reading of AC coefficients
unsigned char ACElement=i;
/* Every AC component is composite of 4 bits (RRRRSSSS). R bits tells us relative position of
non zero element from the previous non zero element (number of zeros between two non zero elements)
SSSS bits tels us magnitude range of AC element
Two special values:
00 is END OF BLOCK (all AC elements are zeros)
F0 is 16 zeroes */
if (ACElement == 0x00) {
return endOfFile;
}
else if (ACElement == 0xF0) {
for (int k=0;k<16;k++) {
dataBlock[m] = 0;
m++;
if (m >= ACcount+1) {
//qDebug() << "Huffman error: 16 AC zeros requested, but only "<<k<<" left in block!";
return endOfFile;
}
}
}
else {
/* If AC element is 0xAB for example, then we have to separate it in two nibbles
First nible is RRRR bits, second are SSSS bits
RRRR bits told us how many zero elements are before this element
SSSS bits told us how many binary digits our AC element has (if 1001 then we have to read next 9 elements from file) */
// Let's separate byte to two nibles
unsigned char Rbits = ACElement >> 4;
unsigned char Sbits = ACElement & 0x0F;
// Before our element there is Rbits zero elements
for (int k=0; k<Rbits; k++) {
if (m >= ACcount) {
//qDebug() << "Huffman error: "<<Rbits<<" preceeding AC zeros requested, but only "<<k<<" left in block!";
// in case of error, doing the other stuff will just do more errors so return here
return endOfFile;
}
dataBlock[m] = 0;
m++;
}
// Do we need to read more bits of data?
while (currentDataLength<Sbits) {
if (!readMoreData(bytes, readPosition, data, currentDataLength)) {
endOfFile = true;
//qDebug() << "End of file encountered inside a Huffman code!";
break;
}
byteno++;
}
// Read out AC coefficient
int ACCoeficient = data >> (currentDataLength-Sbits);
currentDataLength -= Sbits;
data = data - (ACCoeficient<<currentDataLength);
// If MSB in AC coefficient starts with 0, then substract value of AC with 2^bitLength+1
if ( Sbits != 0 && (ACCoeficient>>(Sbits-1)) == 0 ) {
ACCoeficient = ACCoeficient - (2 << (Sbits-1)) + 1;
}
dataBlock[m] = ACCoeficient;
m++;
}
// End of block
if (m >= ACcount+1)
return endOfFile;
if (currentDataLength<3) // If currentData length is < 3, we need to read new byte, so leave this for loop
break;
i = -1; // currentDataLength is not zero, set i=0 to start from first element of array
}
}
}
} while(readMoreData(bytes, readPosition, data, currentDataLength));
endOfFile = true; // We reached an end
return endOfFile;
}
NSData * _Nullable writeDCTBlocks(int width, int height, float const *coefficients) {
NSMutableData *result = [[NSMutableData alloc] initWithCapacity:width * 4 * height];
BitWriter bitWriter([result](unsigned char byte) {
[result appendBytes:&byte length:1];
});
BitCode codewordsArray[2 * CodeWordLimit]; // note: quantized[i] is found at codewordsArray[quantized[i] + CodeWordLimit]
BitCode* codewords = &codewordsArray[CodeWordLimit]; // allow negative indices, so quantized[i] is at codewords[quantized[i]]
uint8_t numBits = 1; // each codeword has at least one bit (value == 0 is undefined)
int32_t mask = 1; // mask is always 2^numBits - 1, initial value 2^1-1 = 2-1 = 1
for (int16_t value = 1; value < CodeWordLimit; value++)
{
// numBits = position of highest set bit (ignoring the sign)
// mask = (2^numBits) - 1
if (value > mask) // one more bit ?
{
numBits++;
mask = (mask << 1) | 1; // append a set bit
}
codewords[-value] = BitCode(mask - value, numBits); // note that I use a negative index => codewords[-value] = codewordsArray[CodeWordLimit value]
codewords[+value] = BitCode( value, numBits);
}
BitCode huffmanLuminanceDC[256];
BitCode huffmanLuminanceAC[256];
memset(huffmanLuminanceDC, 0, sizeof(BitCode) * 256);
memset(huffmanLuminanceAC, 0, sizeof(BitCode) * 256);
generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC);
generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC);
int16_t lastYDC = 0;
float Y[8 * 8];
for (auto blockY = 0; blockY < height; blockY += 8) {
for (auto blockX = 0; blockX < width; blockX += 8) {
for (auto y = 0; y < 8; y++) {
for (auto x = 0; x < 8; x++) {
Y[y * 8 + x] = coefficients[(blockY + y) * width + blockX + x];
}
}
lastYDC = encodeDCTBlock(bitWriter, Y, lastYDC, huffmanLuminanceDC, huffmanLuminanceAC, codewords);
}
}
//bitWriter.flush();
return result;
}
} // namespace TooJpeg
extern "C"
NSData * _Nullable writeDCTBlocks(int width, int height, float const *coefficients) {
NSData *result = DctHuffman::writeDCTBlocks(width, height, coefficients);
/*std::vector<uint8_t> bytes((uint8_t *)result.bytes, ((uint8_t *)result.bytes) + result.length);
int readPosition = 0;
int targetY[8 * 8];
int Y[8 * 8];
int Yzig[8 * 8];
int previousDC = 0;
unsigned int data = 0;
unsigned int currentDataLength = 0;
BitCode huffmanLuminanceDC[256];
BitCode huffmanLuminanceAC[256];
memset(huffmanLuminanceDC, 0, sizeof(BitCode) * 256);
memset(huffmanLuminanceAC, 0, sizeof(BitCode) * 256);
generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC);
generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC);
for (auto blockY = 0; blockY < height; blockY += 8) {
for (auto blockX = 0; blockX < width; blockX += 8) {
for (auto y = 0; y < 8; y++) {
for (auto x = 0; x < 8; x++) {
targetY[y * 8 + x] = coefficients[(blockY + y) * width + blockX + x];
}
}
TooJpeg::readHuffmanBlock(bytes, readPosition, Yzig, data, currentDataLength, 0, huffmanLuminanceDC, huffmanLuminanceAC, previousDC);
for (int i = 0; i < 64; i++) {
Y[i] = Yzig[ZigZag[i]];
}
for (auto y = 0; y < 8; y++) {
for (auto x = 0; x < 8; x++) {
if (Y[y * 8 + x] != targetY[y * 8 + x]) {
printf("fail\n");
}
}
}
}
}*/
return result;
}
extern "C"
void readDCTBlocks(int width, int height, NSData * _Nonnull blockData, float *coefficients, int elementsPerRow) {
std::vector<uint8_t> bytes((uint8_t *)blockData.bytes, ((uint8_t *)blockData.bytes) + blockData.length);
int readPosition = 0;
int Yzig[8 * 8];
int previousDC = 0;
unsigned int data = 0;
unsigned int currentDataLength = 0;
BitCode huffmanLuminanceDC[256];
BitCode huffmanLuminanceAC[256];
memset(huffmanLuminanceDC, 0, sizeof(BitCode) * 256);
memset(huffmanLuminanceAC, 0, sizeof(BitCode) * 256);
generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC);
generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC);
for (auto blockY = 0; blockY < height; blockY += 8) {
for (auto blockX = 0; blockX < width; blockX += 8) {
DctHuffman::readHuffmanBlock(bytes, readPosition, Yzig, data, currentDataLength, 0, huffmanLuminanceDC, huffmanLuminanceAC, previousDC);
for (int i = 0; i < 64; i++) {
coefficients[(blockY + (i / 8)) * elementsPerRow + blockX + (i % 8)] = Yzig[ZigZag[i]];
}
}
}
for (auto blockY = height - 8; blockY < height; blockY += 8) {
for (auto blockX = width - 8; blockX < width; blockX += 8) {
for (int i = 0; i < 64; i++) {
coefficients[(blockY + (i / 8)) * elementsPerRow + blockX + (i % 8)] = 0.0f;
}
}
}
}
@@ -0,0 +1,485 @@
#include <metal_stdlib>
using namespace metal;
half4 yuva(half4 rgba) {
half y = (0.257f * rgba.r) + (0.504 * rgba.g) + (0.098 * rgba.b) + (16.0f / 256.0f);
half v = (0.439 * rgba.r) - (0.368 * rgba.g) - (0.071 * rgba.b) + (128.0f / 256.0f);
half u = -(0.148 * rgba.r) - (0.291 * rgba.g) + (0.439 * rgba.b) + (128.0f / 256.0f);
return half4(y, u, v, rgba.a);
}
half4 rgb(half4 yuva) {
half y = yuva.r - 16.0f / 256.0f;
half u = yuva.g - 128.0f / 256.0f;
half v = yuva.b - 128.0f / 256.0f;
half b = 1.164 * y + 2.018 * u;
half g = 1.164 * y - 0.813 * v - 0.391 * u;
half r = 1.164 * y + 1.596 * v;
return half4(r, g, b, yuva.a);
}
typedef struct {
vector_float2 position;
vector_float2 textureCoordinate;
} Vertex;
constant Vertex quadVertices[6] = {
{{ 2.0, 0.0 }, { 1.0, 1.0 }},
{{ 0.0, 0.0 }, { 0.0, 1.0 }},
{{ 0.0, 2.0 }, { 0.0, 0.0 }},
{{ 2.0, 0.0 }, { 1.0, 1.0 }},
{{ 0.0, 2.0 }, { 0.0, 0.0 }},
{{ 2.0, 2.0 }, { 1.0, 0.0 }}
};
struct RasterizerData {
float4 clipSpacePosition [[position]];
float2 textureCoordinate;
};
vertex RasterizerData vertexShader(
uint vid [[vertex_id]]
) {
RasterizerData out;
float2 pixelSpacePosition = quadVertices[vid].position.xy;
pixelSpacePosition.x -= 1.0f;
pixelSpacePosition.y -= 1.0f;
out.clipSpacePosition.xy = pixelSpacePosition;
out.clipSpacePosition.z = 0.0f;
out.clipSpacePosition.w = 1.0f;
out.textureCoordinate = quadVertices[vid].textureCoordinate;
return out;
}
fragment float4 samplingIdctShader(
RasterizerData in [[stage_in]],
texture2d<half, access::sample> colorTexture0 [[texture(0)]],
texture2d<half, access::sample> colorTexture1 [[texture(1)]],
texture2d<half, access::sample> colorTexture2 [[texture(2)]],
texture2d<half, access::sample> colorTexture3 [[texture(3)]]
) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
const half color0 = colorTexture0.sample(textureSampler, in.textureCoordinate).r;
const half color1 = colorTexture1.sample(textureSampler, in.textureCoordinate).r;
const half color2 = colorTexture2.sample(textureSampler, in.textureCoordinate).r;
const half color3 = colorTexture3.sample(textureSampler, in.textureCoordinate).r;
const half4 yuva = half4(color0, color1, color2, color3);
const half4 color = rgb(yuva);
return float4(color.r * color.a, color.g * color.a, color.b * color.a, color.a);
}
fragment float4 samplingRgbShader(
RasterizerData in [[stage_in]],
texture2d<half, access::sample> colorTexture [[texture(0)]]
) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
half4 color = colorTexture.sample(textureSampler, in.textureCoordinate);
color.r *= color.a;
color.g *= color.a;
color.b *= color.a;
return float4(color.r, color.g, color.b, color.a);
}
half4 samplePoint(texture2d<half, access::sample> textureY, texture2d<half, access::sample> textureCbCr, sampler s, float2 texcoord) {
half y;
half2 uv;
y = textureY.sample(s, texcoord).r;
uv = textureCbCr.sample(s, texcoord).rg - half2(0.5, 0.5);
// Conversion for YUV to rgb from http://www.fourcc.org/fccyvrgb.php
half4 out = half4(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
return out;
}
fragment float4 samplingYuvaShader(
RasterizerData in [[stage_in]],
texture2d<half, access::sample> yTexture [[texture(0)]],
texture2d<half, access::sample> cbcrTexture [[texture(1)]],
texture2d<uint, access::read> alphaTexture [[texture(2)]],
constant uint2 &alphaSize [[buffer(3)]]
) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
half4 color = samplePoint(yTexture, cbcrTexture, textureSampler, in.textureCoordinate);
int alphaX = (int)(in.textureCoordinate.x * alphaSize.x);
int alphaY = (int)(in.textureCoordinate.y * alphaSize.y);
uint32_t packedAlpha = alphaTexture.read(uint2(alphaX / 2, alphaY)).r;
uint32_t a1 = (packedAlpha & (0xf0U));
uint32_t a2 = (packedAlpha & (0x0fU)) << 4;
uint32_t left = (a1 >> 4) | a1;
uint32_t right = (a2 >> 4) | a2;
uint32_t chooseLeft = alphaX % 2 == 0;
uint32_t resolvedAlpha = chooseLeft * left + (1 - chooseLeft) * right;
float alpha = resolvedAlpha / 255.0f;
color.r *= alpha;
color.g *= alpha;
color.b *= alpha;
color.a = alpha;
return float4(color);
}
#define BLOCK_SIZE 8
#define BLOCK_SIZE2 BLOCK_SIZE * BLOCK_SIZE
#define BLOCK_SIZE_LOG2 3
#define chromaQp 60
#define lumaQp 70
#define alphaQp 60
constant float DCTv8matrix[] = {
0.3535533905932738f, 0.4903926402016152f, 0.4619397662556434f, 0.4157348061512726f, 0.3535533905932738f, 0.2777851165098011f, 0.1913417161825449f, 0.0975451610080642f,
0.3535533905932738f, 0.4157348061512726f, 0.1913417161825449f, -0.0975451610080641f, -0.3535533905932737f, -0.4903926402016152f, -0.4619397662556434f, -0.2777851165098011f,
0.3535533905932738f, 0.2777851165098011f, -0.1913417161825449f, -0.4903926402016152f, -0.3535533905932738f, 0.0975451610080642f, 0.4619397662556433f, 0.4157348061512727f,
0.3535533905932738f, 0.0975451610080642f, -0.4619397662556434f, -0.2777851165098011f, 0.3535533905932737f, 0.4157348061512727f, -0.1913417161825450f, -0.4903926402016153f,
0.3535533905932738f, -0.0975451610080641f, -0.4619397662556434f, 0.2777851165098009f, 0.3535533905932738f, -0.4157348061512726f, -0.1913417161825453f, 0.4903926402016152f,
0.3535533905932738f, -0.2777851165098010f, -0.1913417161825452f, 0.4903926402016153f, -0.3535533905932733f, -0.0975451610080649f, 0.4619397662556437f, -0.4157348061512720f,
0.3535533905932738f, -0.4157348061512727f, 0.1913417161825450f, 0.0975451610080640f, -0.3535533905932736f, 0.4903926402016152f, -0.4619397662556435f, 0.2777851165098022f,
0.3535533905932738f, -0.4903926402016152f, 0.4619397662556433f, -0.4157348061512721f, 0.3535533905932733f, -0.2777851165098008f, 0.1913417161825431f, -0.0975451610080625f
};
constant float baseQLuma[BLOCK_SIZE2] = {
16.0f, 11.0f, 10.0f, 16.0f, 24.0f, 40.0f, 51.0f, 61.0f,
12.0f, 12.0f, 14.0f, 19.0f, 26.0f, 58.0f, 60.0f, 55.0f,
14.0f, 13.0f, 16.0f, 24.0f, 40.0f, 57.0f, 69.0f, 56.0f,
14.0f, 17.0f, 22.0f, 29.0f, 51.0f, 87.0f, 80.0f, 62.0f,
18.0f, 22.0f, 37.0f, 56.0f, 68.0f, 109.0f, 103.0f, 77.0f,
24.0f, 35.0f, 55.0f, 64.0f, 81.0f, 104.0f, 113.0f, 92.0f,
49.0f, 64.0f, 78.0f, 87.0f, 103.0f, 121.0f, 120.0f, 101.0f,
72.0f, 92.0f, 95.0f, 98.0f, 112.0f, 100.0f, 103.0f, 99.0f
};
constant float baseQChroma[BLOCK_SIZE2] = {
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99
};
float adjustQ(int qp, int index, bool isChroma) {
float baseValue;
if (isChroma) {
baseValue = baseQChroma[index];
} else {
baseValue = baseQLuma[index];
}
float s = 0.0f;
if (qp < 50) {
s = 5000.0f / (float)qp;
} else {
s = 200.0 - (2.0 * (float)qp);
}
float r = floor(s * baseValue + 50.0f) / 100.0f;
return r;
}
void copyTextureBlockIn(
half4 inColorRgb,
int colorPlane,
uint2 blockPosition,
threadgroup float *block
) {
half4 inColor = yuva(inColorRgb);
half color;
if (colorPlane == 0) {
color = inColor.r;
} else if (colorPlane == 1) {
color = inColor.g;
} else if (colorPlane == 2) {
color = inColor.b;
} else {
color = inColor.a;
}
block[(blockPosition.y << BLOCK_SIZE_LOG2) + blockPosition.x] = color;
}
void copyTextureBlockInDequantize(
texture2d<half, access::read> texture,
uint2 pixelPosition,
uint2 blockPosition,
threadgroup float *block,
int qp,
bool isChroma
) {
half inColor = (half)texture.read(pixelPosition).r;
int index = (blockPosition.y << BLOCK_SIZE_LOG2) + blockPosition.x;
float q = adjustQ(qp, index, isChroma);
float dequantized = inColor * q;
block[index] = dequantized;
}
void copyTextureBlockOut(
uint2 pixelPosition,
uint2 blockPosition,
threadgroup float *block,
texture2d<half, access::write> texture
) {
half result = block[(blockPosition.y << BLOCK_SIZE_LOG2) + blockPosition.x];
texture.write(half4(result, result, result, 1.0), pixelPosition);
}
void copyTextureBlockOutFloat(
uint2 pixelPosition,
uint2 blockPosition,
threadgroup float *block,
texture2d<half, access::write> texture
) {
int rawIndex = (blockPosition.y << BLOCK_SIZE_LOG2) + blockPosition.x;
int index = rawIndex;
half result = block[index];
texture.write(half(result), pixelPosition);
}
void reorderBlockZigzag(threadgroup float *blockIn, threadgroup float *blockOut, uint2 blockPosition) {
int rawIndex = (blockPosition.y << BLOCK_SIZE_LOG2) + blockPosition.x;
int index = rawIndex;
blockOut[index] = blockIn[rawIndex];
}
void DCT(
uint2 blockPosition,
threadgroup float *CurBlockLocal1,
threadgroup float *CurBlockLocal2
) {
int tx = blockPosition.x;
int ty = blockPosition.y;
float curelem = 0;
int DCTv8matrixIndex = 0 * BLOCK_SIZE + ty;
int CurBlockLocal1Index = 0 * BLOCK_SIZE + tx;
#pragma unroll
for (int i=0; i < BLOCK_SIZE; i++)
{
curelem += DCTv8matrix[DCTv8matrixIndex] * (CurBlockLocal1[CurBlockLocal1Index] * 255.0f - 128.0f);
DCTv8matrixIndex += BLOCK_SIZE;
CurBlockLocal1Index += BLOCK_SIZE;
}
CurBlockLocal2[(ty << BLOCK_SIZE_LOG2) + tx] = curelem;
threadgroup_barrier(mem_flags::mem_threadgroup);
curelem = 0;
int CurBlockLocal2Index = (ty << BLOCK_SIZE_LOG2) + 0;
DCTv8matrixIndex = 0 * BLOCK_SIZE + tx;
#pragma unroll
for (int i=0; i<BLOCK_SIZE; i++)
{
curelem += CurBlockLocal2[CurBlockLocal2Index] * DCTv8matrix[DCTv8matrixIndex];
CurBlockLocal2Index += 1;
DCTv8matrixIndex += BLOCK_SIZE;
}
CurBlockLocal1[(ty << BLOCK_SIZE_LOG2) + tx ] = curelem;
}
void IDCT(
uint2 blockPosition,
threadgroup float *CurBlockLocal1,
threadgroup float *CurBlockLocal2
) {
int tx = blockPosition.x;
int ty = blockPosition.y;
float curelem = 0;
int DCTv8matrixIndex = (ty << BLOCK_SIZE_LOG2) + 0;
int CurBlockLocal1Index = 0 * BLOCK_SIZE + tx;
#pragma unroll
for (int i=0; i<BLOCK_SIZE; i++)
{
curelem += DCTv8matrix[DCTv8matrixIndex] * CurBlockLocal1[CurBlockLocal1Index];
DCTv8matrixIndex += 1;
CurBlockLocal1Index += BLOCK_SIZE;
}
CurBlockLocal2[(ty << BLOCK_SIZE_LOG2) + tx ] = curelem;
threadgroup_barrier(mem_flags::mem_threadgroup);
curelem = 0;
int CurBlockLocal2Index = (ty << BLOCK_SIZE_LOG2) + 0;
DCTv8matrixIndex = (tx << BLOCK_SIZE_LOG2) + 0;
#pragma unroll
for (int i=0; i<BLOCK_SIZE; i++)
{
curelem += CurBlockLocal2[CurBlockLocal2Index] * DCTv8matrix[DCTv8matrixIndex];
CurBlockLocal2Index += 1;
DCTv8matrixIndex += 1;
}
CurBlockLocal1[(ty << BLOCK_SIZE_LOG2) + tx ] = (curelem + 128.0f) / 255.0f;
}
void quantize(
int qp,
threadgroup float *sourceBlock,
threadgroup float *destinationBlock,
int index,
bool isChroma
) {
float q = adjustQ(qp, index, isChroma);
float value = sourceBlock[index];
float quantized = round(value / q);
destinationBlock[index] = quantized;
}
void dequantize(
int qp,
threadgroup float *sourceBlock,
threadgroup float *destinationBlock,
int index,
bool isChroma
) {
float q = adjustQ(qp, index, isChroma);
float value = sourceBlock[index];
float dequantized = value * q;
destinationBlock[index] = dequantized;
}
kernel void dctKernel(
texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 pixelPosition [[thread_position_in_grid]],
uint2 blockPosition [[thread_position_in_threadgroup]],
constant int &colorPlane [[buffer(2)]]
) {
threadgroup float CurBlockLocal1[BLOCK_SIZE2];
threadgroup float CurBlockLocal2[BLOCK_SIZE2];
half4 rgbPixelIn;
int imageQp;
bool isChroma = false;
if (colorPlane == 1 || colorPlane == 2) {
imageQp = chromaQp;
isChroma = true;
half4 rgbPixelIn0 = inTexture.read(uint2(pixelPosition.x * 2, pixelPosition.y * 2));
half4 rgbPixelNextX = inTexture.read(uint2(pixelPosition.x * 2 + 1, pixelPosition.y * 2));
half4 rgbPixelNextY = inTexture.read(uint2(pixelPosition.x * 2, pixelPosition.y * 2 + 1));
half4 rgbPixelNextXY = inTexture.read(uint2(pixelPosition.x * 2 + 1, pixelPosition.y * 2 + 1));
rgbPixelIn = mix(rgbPixelIn0, rgbPixelNextX, 0.5);
rgbPixelIn = mix(rgbPixelIn, rgbPixelNextY, 0.5);
rgbPixelIn = mix(rgbPixelIn, rgbPixelNextXY, 0.5);
} else {
if (colorPlane == 3) {
imageQp = alphaQp;
} else {
imageQp = lumaQp;
}
rgbPixelIn = inTexture.read(pixelPosition);
}
copyTextureBlockIn(rgbPixelIn, colorPlane, blockPosition, CurBlockLocal1);
threadgroup_barrier(mem_flags::mem_threadgroup);
DCT(
blockPosition,
CurBlockLocal1,
CurBlockLocal2
);
threadgroup_barrier(mem_flags::mem_threadgroup);
int index = (blockPosition.y << BLOCK_SIZE_LOG2) + blockPosition.x;
quantize(imageQp, CurBlockLocal1, CurBlockLocal2, index, isChroma);
threadgroup_barrier(mem_flags::mem_threadgroup);
reorderBlockZigzag(CurBlockLocal2, CurBlockLocal1, blockPosition);
threadgroup_barrier(mem_flags::mem_threadgroup);
copyTextureBlockOutFloat(
pixelPosition,
blockPosition,
CurBlockLocal1,
outTexture
);
}
kernel void idctKernel(
texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 pixelPosition [[thread_position_in_grid]],
uint2 blockPosition [[thread_position_in_threadgroup]],
constant int &colorPlane [[buffer(2)]]
) {
threadgroup float CurBlockLocal1[BLOCK_SIZE2];
threadgroup float CurBlockLocal2[BLOCK_SIZE2];
int imageQp;
bool isChroma = false;
if (colorPlane == 1 || colorPlane == 2) {
isChroma = true;
imageQp = chromaQp;
} else {
if (colorPlane == 3) {
imageQp = alphaQp;
} else {
imageQp = lumaQp;
}
}
copyTextureBlockInDequantize(inTexture, pixelPosition, blockPosition, CurBlockLocal1, imageQp, isChroma);
threadgroup_barrier(mem_flags::mem_threadgroup);
IDCT(
blockPosition,
CurBlockLocal1,
CurBlockLocal2
);
threadgroup_barrier(mem_flags::mem_threadgroup);
copyTextureBlockOut(
pixelPosition,
blockPosition,
CurBlockLocal1,
outTexture
);
}
@@ -0,0 +1,393 @@
import Foundation
import Metal
import DctHuffman
private final class BundleHelper: NSObject {
}
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
final class Texture {
final class DirectBuffer {
let buffer: MTLBuffer
let bytesPerRow: Int
init?(device: MTLDevice, width: Int, height: Int, bytesPerRow: Int) {
#if targetEnvironment(simulator)
return nil
#else
if #available(iOS 12.0, *) {
let pagesize = Int(getpagesize())
let allocationSize = alignUp(size: bytesPerRow * height, align: pagesize)
var data: UnsafeMutableRawPointer? = nil
let result = posix_memalign(&data, pagesize, allocationSize)
if result == noErr, let data = data {
self.bytesPerRow = bytesPerRow
guard let buffer = device.makeBuffer(
bytesNoCopy: data,
length: allocationSize,
options: .storageModeShared,
deallocator: { _, _ in
free(data)
}
) else {
return nil
}
self.buffer = buffer
} else {
return nil
}
} else {
return nil
}
#endif
}
}
let width: Int
let height: Int
let texture: MTLTexture
let directBuffer: DirectBuffer?
init?(
device: MTLDevice,
width: Int,
height: Int,
pixelFormat: MTLPixelFormat,
usage: MTLTextureUsage,
isShared: Bool
) {
self.width = width
self.height = height
if #available(iOS 12.0, *), isShared, usage.contains(.shaderRead) {
switch pixelFormat {
case .r32Float, .bgra8Unorm:
let bytesPerPixel = 4
let pixelRowAlignment = device.minimumTextureBufferAlignment(for: pixelFormat)
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
self.directBuffer = DirectBuffer(device: device, width: width, height: height, bytesPerRow: bytesPerRow)
case .r8Unorm, .r8Uint:
let bytesPerPixel = 1
let pixelRowAlignment = device.minimumTextureBufferAlignment(for: pixelFormat)
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
self.directBuffer = DirectBuffer(device: device, width: width, height: height, bytesPerRow: bytesPerRow)
case .rg8Unorm:
let bytesPerPixel = 2
let pixelRowAlignment = device.minimumTextureBufferAlignment(for: pixelFormat)
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
self.directBuffer = DirectBuffer(device: device, width: width, height: height, bytesPerRow: bytesPerRow)
default:
self.directBuffer = nil
}
} else {
self.directBuffer = nil
}
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = pixelFormat
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = usage
if let directBuffer = self.directBuffer {
textureDescriptor.storageMode = directBuffer.buffer.storageMode
guard let texture = directBuffer.buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: directBuffer.bytesPerRow) else {
return nil
}
self.texture = texture
} else {
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
self.texture = texture
}
}
func replace(with image: AnimationCompressor.ImageData) {
if image.width != self.width || image.height != self.height {
assert(false, "Image size does not match")
return
}
let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: image.width, height: image.height, depth: 1))
if let directBuffer = self.directBuffer, directBuffer.bytesPerRow == image.bytesPerRow {
image.data.withUnsafeBytes { bytes in
let _ = memcpy(directBuffer.buffer.contents(), bytes.baseAddress!, image.bytesPerRow * self.height)
}
} else {
image.data.withUnsafeBytes { bytes in
self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes.baseAddress!, bytesPerRow: image.bytesPerRow)
}
}
}
func readDirect(width: Int, height: Int, bytesPerRow: Int, read: (UnsafeMutableRawPointer?) -> UnsafeRawPointer) {
if let directBuffer = self.directBuffer, width == self.width, height == self.height, bytesPerRow == directBuffer.bytesPerRow {
let _ = read(directBuffer.buffer.contents())
} else {
let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: width, height: height, depth: 1))
self.texture.replace(region: region, mipmapLevel: 0, withBytes: read(nil), bytesPerRow: bytesPerRow)
}
}
}
final class TextureSet {
struct Description {
let fractionWidth: Int
let fractionHeight: Int
let pixelFormat: MTLPixelFormat
}
let width: Int
let height: Int
let textures: [Texture]
init?(
device: MTLDevice,
width: Int,
height: Int,
descriptions: [Description],
usage: MTLTextureUsage,
isShared: Bool
) {
self.width = width
self.height = height
var textures: [Texture] = []
for i in 0 ..< descriptions.count {
let planeWidth = width / descriptions[i].fractionWidth
let planeHeight = height / descriptions[i].fractionHeight
guard let texture = Texture(
device: device,
width: planeWidth,
height: planeHeight,
pixelFormat: descriptions[i].pixelFormat,
usage: usage,
isShared: isShared
) else {
return nil
}
textures.append(texture)
}
self.textures = textures
}
}
public final class AnimationCompressor {
public final class ImageData {
public let width: Int
public let height: Int
public let bytesPerRow: Int
public let data: Data
public init(width: Int, height: Int, bytesPerRow: Int, data: Data) {
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
self.data = data
}
}
public final class CompressedImageData {
public let data: Data
public init(data: Data) {
self.data = data
}
}
public final class SharedContext {
public static let shared: SharedContext = SharedContext()!
public let device: MTLDevice
let defaultLibrary: MTLLibrary
private let computeDctPipelineState: MTLComputePipelineState
private let commandQueue: MTLCommandQueue
public init?() {
guard let device = MTLCreateSystemDefaultDevice() else {
return nil
}
self.device = device
let mainBundle = Bundle(for: BundleHelper.self)
guard let path = mainBundle.path(forResource: "AnimationCompressionBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
if #available(iOS 10.0, *) {
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
self.defaultLibrary = defaultLibrary
} else {
preconditionFailure()
}
guard let dctFunction = self.defaultLibrary.makeFunction(name: "dctKernel") else {
return nil
}
guard let computeDctPipelineState = try? self.device.makeComputePipelineState(function: dctFunction) else {
return nil
}
self.computeDctPipelineState = computeDctPipelineState
guard let commandQueue = self.device.makeCommandQueue() else {
return nil
}
self.commandQueue = commandQueue
}
func compress(compressor: AnimationCompressor, image: ImageData, completion: @escaping (CompressedImageData) -> Void) {
let threadgroupSize = MTLSize(width: 8, height: 8, depth: 1)
assert(image.width % 8 == 0)
assert(image.height % 8 == 0)
let inputTexture: Texture
if let current = compressor.inputTexture, current.width == image.width, current.height == image.height {
inputTexture = current
} else {
guard let texture = Texture(
device: self.device,
width: image.width,
height: image.height,
pixelFormat: .bgra8Unorm,
usage: .shaderRead,
isShared: true
) else {
return
}
inputTexture = texture
compressor.inputTexture = texture
}
inputTexture.replace(with: image)
let compressedTextures: TextureSet
if let current = compressor.compressedTextures, current.width == image.width, current.height == image.height {
compressedTextures = current
} else {
guard let textures = TextureSet(
device: self.device,
width: image.width,
height: image.height,
descriptions: [
TextureSet.Description(
fractionWidth: 1, fractionHeight: 1,
pixelFormat: .r32Float
),
TextureSet.Description(
fractionWidth: 2, fractionHeight: 2,
pixelFormat: .r32Float
),
TextureSet.Description(
fractionWidth: 2, fractionHeight: 2,
pixelFormat: .r32Float
),
TextureSet.Description(
fractionWidth: 1, fractionHeight: 1,
pixelFormat: .r32Float
)
],
usage: [.shaderWrite],
isShared: false
) else {
return
}
compressedTextures = textures
compressor.compressedTextures = textures
}
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
commandBuffer.label = "ImageCompressor"
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return
}
computeEncoder.setComputePipelineState(self.computeDctPipelineState)
computeEncoder.setTexture(inputTexture.texture, index: 0)
for colorPlane in 0 ..< 4 {
computeEncoder.setTexture(compressedTextures.textures[colorPlane].texture, index: 1)
var colorPlaneInt32 = Int32(colorPlane)
computeEncoder.setBytes(&colorPlaneInt32, length: 4, index: 2)
let threadgroupCount = MTLSize(width: (compressedTextures.textures[colorPlane].width + threadgroupSize.width - 1) / threadgroupSize.width, height: (compressedTextures.textures[colorPlane].height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
}
computeEncoder.endEncoding()
commandBuffer.addCompletedHandler { _ in
let buffer = WriteBuffer()
buffer.writeInt32(0x543ee445)
buffer.writeInt32(4)
buffer.writeInt32(Int32(compressedTextures.textures[0].width))
buffer.writeInt32(Int32(compressedTextures.textures[0].height))
for i in 0 ..< 4 {
let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: compressedTextures.textures[i].width, height: compressedTextures.textures[i].height, depth: 1))
let bytesPerRow = 4 * compressedTextures.textures[i].width
buffer.writeInt32(Int32(compressedTextures.textures[i].width))
buffer.writeInt32(Int32(compressedTextures.textures[i].height))
buffer.writeInt32(Int32(bytesPerRow))
var textureBytes = Data(count: bytesPerRow * compressedTextures.textures[i].height)
textureBytes.withUnsafeMutableBytes { bytes in
compressedTextures.textures[i].texture.getBytes(bytes.baseAddress!, bytesPerRow: bytesPerRow, bytesPerImage: bytesPerRow * compressedTextures.textures[i].height, from: region, mipmapLevel: 0, slice: 0)
let huffmanData = writeDCTBlocks(Int32(compressedTextures.textures[i].width), Int32(compressedTextures.textures[i].height), bytes.baseAddress!.assumingMemoryBound(to: Float32.self))!
buffer.writeInt32(Int32(huffmanData.count))
buffer.write(huffmanData)
}
}
DispatchQueue.main.async {
completion(CompressedImageData(data: buffer.makeData()))
}
}
commandBuffer.commit()
}
}
private let sharedContext: SharedContext
private var inputTexture: Texture?
private var compressedTextures: TextureSet?
public init(sharedContext: SharedContext) {
self.sharedContext = sharedContext
}
public func compress(image: ImageData, completion: @escaping (CompressedImageData) -> Void) {
self.sharedContext.compress(compressor: self, image: image, completion: completion)
}
}
@@ -0,0 +1,110 @@
import Foundation
class MemoryBuffer {
var data: Data
var length: Int
init(data: Data) {
self.data = data
self.length = data.count
}
}
final class WriteBuffer: MemoryBuffer {
var offset = 0
init() {
super.init(data: Data())
}
func makeData() -> Data {
return self.data
}
func reset() {
self.offset = 0
}
func write(_ data: UnsafeRawPointer, offset: Int = 0, length: Int) {
if self.offset + length > self.data.count {
self.data.count = self.offset + length + 256
}
self.data.withUnsafeMutableBytes { bytes in
let _ = memcpy(bytes.baseAddress!.advanced(by: self.offset), data + offset, length)
}
self.offset += length
self.length = self.offset
}
func write(_ data: Data) {
data.withUnsafeBytes { bytes in
self.write(bytes.baseAddress!, length: bytes.count)
}
}
func writeInt8(_ value: Int8) {
var value = value
self.write(&value, length: 1)
}
func writeInt32(_ value: Int32) {
var value = value
self.write(&value, length: 4)
}
func writeFloat(_ value: Float) {
var value: Float32 = value
self.write(&value, length: 4)
}
func seek(offset: Int) {
self.offset = offset
}
}
final class ReadBuffer: MemoryBuffer {
var offset = 0
override init(data: Data) {
super.init(data: data)
}
func read(_ data: UnsafeMutableRawPointer, length: Int) {
self.data.copyBytes(to: data.assumingMemoryBound(to: UInt8.self), from: self.offset ..< (self.offset + length))
self.offset += length
}
func readDataNoCopy(length: Int) -> Data {
let result = self.data.withUnsafeBytes { bytes -> Data in
return Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: bytes.baseAddress!.advanced(by: self.offset)), count: length, deallocator: .none)
}
self.offset += length
return result
}
func readInt8() -> Int8 {
var result: Int8 = 0
self.read(&result, length: 1)
return result
}
func readInt32() -> Int32 {
var result: Int32 = 0
self.read(&result, length: 4)
return result
}
func readFloat() -> Float {
var result: Float32 = 0
self.read(&result, length: 4)
return result
}
func skip(_ length: Int) {
self.offset += length
}
func reset() {
self.offset = 0
}
}
@@ -0,0 +1,545 @@
import Foundation
import UIKit
import Metal
import MetalKit
import simd
import DctHuffman
import MetalImageView
private struct Vertex {
var position: vector_float2
var textureCoordinate: vector_float2
}
public final class CompressedImageRenderer {
private final class Shared {
static let shared: Shared = {
return Shared(sharedContext: AnimationCompressor.SharedContext.shared)!
}()
let sharedContext: AnimationCompressor.SharedContext
let computeIdctPipelineState: MTLComputePipelineState
let renderIdctPipelineState: MTLRenderPipelineState
let renderRgbPipelineState: MTLRenderPipelineState
let renderYuvaPipelineState: MTLRenderPipelineState
init?(sharedContext: AnimationCompressor.SharedContext) {
self.sharedContext = sharedContext
guard let idctFunction = self.sharedContext.defaultLibrary.makeFunction(name: "idctKernel") else {
return nil
}
guard let computeIdctPipelineState = try? self.sharedContext.device.makeComputePipelineState(function: idctFunction) else {
return nil
}
self.computeIdctPipelineState = computeIdctPipelineState
guard let vertexShader = self.sharedContext.defaultLibrary.makeFunction(name: "vertexShader") else {
return nil
}
guard let samplingIdctShader = self.sharedContext.defaultLibrary.makeFunction(name: "samplingIdctShader") else {
return nil
}
guard let samplingRgbShader = self.sharedContext.defaultLibrary.makeFunction(name: "samplingRgbShader") else {
return nil
}
guard let samplingYuvaShader = self.sharedContext.defaultLibrary.makeFunction(name: "samplingYuvaShader") else {
return nil
}
let idctPipelineStateDescriptor = MTLRenderPipelineDescriptor()
idctPipelineStateDescriptor.label = "Render IDCT Pipeline"
idctPipelineStateDescriptor.vertexFunction = vertexShader
idctPipelineStateDescriptor.fragmentFunction = samplingIdctShader
idctPipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let renderIdctPipelineState = try? self.sharedContext.device.makeRenderPipelineState(descriptor: idctPipelineStateDescriptor) else {
return nil
}
self.renderIdctPipelineState = renderIdctPipelineState
let rgbPipelineStateDescriptor = MTLRenderPipelineDescriptor()
rgbPipelineStateDescriptor.label = "Render RGB Pipeline"
rgbPipelineStateDescriptor.vertexFunction = vertexShader
rgbPipelineStateDescriptor.fragmentFunction = samplingRgbShader
rgbPipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let renderRgbPipelineState = try? self.sharedContext.device.makeRenderPipelineState(descriptor: rgbPipelineStateDescriptor) else {
return nil
}
self.renderRgbPipelineState = renderRgbPipelineState
let yuvaPipelineStateDescriptor = MTLRenderPipelineDescriptor()
yuvaPipelineStateDescriptor.label = "Render YUVA Pipeline"
yuvaPipelineStateDescriptor.vertexFunction = vertexShader
yuvaPipelineStateDescriptor.fragmentFunction = samplingYuvaShader
yuvaPipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let renderYuvaPipelineState = try? self.sharedContext.device.makeRenderPipelineState(descriptor: yuvaPipelineStateDescriptor) else {
return nil
}
self.renderYuvaPipelineState = renderYuvaPipelineState
}
}
private let sharedContext: AnimationCompressor.SharedContext
private let shared: Shared
private var compressedTextures: TextureSet?
private var outputTextures: TextureSet?
private var rgbTexture: Texture?
private var yuvaTextures: TextureSet?
private let commandQueue: MTLCommandQueue
private var isRendering: Bool = false
public init?(sharedContext: AnimationCompressor.SharedContext) {
self.sharedContext = sharedContext
self.shared = Shared.shared
guard let commandQueue = self.sharedContext.device.makeCommandQueue() else {
return nil
}
self.commandQueue = commandQueue
}
private var drawableRequestTimestamp: Double?
private func getNextDrawable(layer: MetalImageLayer, drawableSize: CGSize) -> MetalImageLayer.Drawable? {
layer.renderer.drawableSize = drawableSize
return layer.renderer.nextDrawable()
}
private func updateIdctTextures(compressedImage: AnimationCompressor.CompressedImageData) {
self.rgbTexture = nil
self.yuvaTextures = nil
let readBuffer = ReadBuffer(data: compressedImage.data)
if readBuffer.readInt32() != 0x543ee445 {
return
}
if readBuffer.readInt32() != 4 {
return
}
let width = Int(readBuffer.readInt32())
let height = Int(readBuffer.readInt32())
let compressedTextures: TextureSet
if let current = self.compressedTextures, current.width == width, current.height == height {
compressedTextures = current
} else {
guard let textures = TextureSet(
device: self.sharedContext.device,
width: width,
height: height,
descriptions: [
TextureSet.Description(
fractionWidth: 1, fractionHeight: 1,
pixelFormat: .r32Float
),
TextureSet.Description(
fractionWidth: 2, fractionHeight: 2,
pixelFormat: .r32Float
),
TextureSet.Description(
fractionWidth: 2, fractionHeight: 2,
pixelFormat: .r32Float
),
TextureSet.Description(
fractionWidth: 1, fractionHeight: 1,
pixelFormat: .r32Float
)
],
usage: .shaderRead,
isShared: true
) else {
return
}
self.compressedTextures = textures
compressedTextures = textures
}
for i in 0 ..< 4 {
let planeWidth = Int(readBuffer.readInt32())
let planeHeight = Int(readBuffer.readInt32())
let bytesPerRow = Int(readBuffer.readInt32())
let planeSize = Int(readBuffer.readInt32())
let planeData = readBuffer.readDataNoCopy(length: planeSize)
var tempData: Data?
compressedTextures.textures[i].readDirect(width: planeWidth, height: planeHeight, bytesPerRow: bytesPerRow, read: { destinationBytes in
if let destinationBytes = destinationBytes {
readDCTBlocks(Int32(planeWidth), Int32(planeHeight), planeData, destinationBytes.assumingMemoryBound(to: Float32.self), Int32(bytesPerRow / 4))
return UnsafeRawPointer(destinationBytes)
} else {
tempData = Data(count: bytesPerRow * planeHeight)
return tempData!.withUnsafeMutableBytes { bytes -> UnsafeRawPointer in
readDCTBlocks(Int32(planeWidth), Int32(planeHeight), planeData, bytes.baseAddress!.assumingMemoryBound(to: Float32.self), Int32(bytesPerRow / 4))
return UnsafeRawPointer(bytes.baseAddress!)
}
}
})
}
}
public func renderIdct(layer: MetalImageLayer, compressedImage: AnimationCompressor.CompressedImageData, completion: @escaping () -> Void) {
DispatchQueue.global().async {
self.updateIdctTextures(compressedImage: compressedImage)
DispatchQueue.main.async {
guard let compressedTextures = self.compressedTextures else {
return
}
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
commandBuffer.label = "MyCommand"
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return
}
computeEncoder.setComputePipelineState(self.shared.computeIdctPipelineState)
let outputTextures: TextureSet
if let current = self.outputTextures, current.width == compressedTextures.textures[0].width, current.height == compressedTextures.textures[0].height {
outputTextures = current
} else {
guard let textures = TextureSet(
device: self.sharedContext.device,
width: compressedTextures.textures[0].width,
height: compressedTextures.textures[0].height,
descriptions: [
TextureSet.Description(
fractionWidth: 1, fractionHeight: 1,
pixelFormat: .r8Unorm
),
TextureSet.Description(
fractionWidth: 2, fractionHeight: 2,
pixelFormat: .r8Unorm
),
TextureSet.Description(
fractionWidth: 2, fractionHeight: 2,
pixelFormat: .r8Unorm
),
TextureSet.Description(
fractionWidth: 1, fractionHeight: 1,
pixelFormat: .r8Unorm
)
],
usage: [.shaderRead, .shaderWrite],
isShared: false
) else {
return
}
self.outputTextures = textures
outputTextures = textures
}
for i in 0 ..< 4 {
computeEncoder.setTexture(compressedTextures.textures[i].texture, index: 0)
computeEncoder.setTexture(outputTextures.textures[i].texture, index: 1)
var colorPlaneInt32 = Int32(i)
computeEncoder.setBytes(&colorPlaneInt32, length: 4, index: 2)
let threadgroupSize = MTLSize(width: 8, height: 8, depth: 1)
let threadgroupCount = MTLSize(width: (compressedTextures.textures[i].width + threadgroupSize.width - 1) / threadgroupSize.width, height: (compressedTextures.textures[i].height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
}
computeEncoder.endEncoding()
let drawableSize = CGSize(width: CGFloat(outputTextures.textures[0].width), height: CGFloat(outputTextures.textures[0].height))
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
commandBuffer.commit()
completion()
return
}
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
renderEncoder.label = "MyRenderEncoder"
renderEncoder.setRenderPipelineState(self.shared.renderIdctPipelineState)
for i in 0 ..< 4 {
renderEncoder.setFragmentTexture(outputTextures.textures[i].texture, index: i)
}
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
renderEncoder.endEncoding()
var storedDrawable: MetalImageLayer.Drawable? = drawable
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {
autoreleasepool {
storedDrawable?.present(completion: completion)
storedDrawable = nil
}
}
}
commandBuffer.commit()
}
}
}
private func updateRgbTexture(width: Int, height: Int, bytesPerRow: Int, data: Data) {
self.compressedTextures = nil
self.outputTextures = nil
self.yuvaTextures = nil
let rgbTexture: Texture
if let current = self.rgbTexture, current.width == width, current.height == height {
rgbTexture = current
} else {
guard let texture = Texture(device: self.sharedContext.device, width: width, height: height, pixelFormat: .bgra8Unorm, usage: .shaderRead, isShared: true) else {
return
}
self.rgbTexture = texture
rgbTexture = texture
}
rgbTexture.readDirect(width: width, height: height, bytesPerRow: bytesPerRow, read: { destinationBytes in
return data.withUnsafeBytes { bytes -> UnsafeRawPointer in
if let destinationBytes = destinationBytes {
memcpy(destinationBytes, bytes.baseAddress!, bytes.count)
return UnsafeRawPointer(destinationBytes)
} else {
return bytes.baseAddress!
}
}
})
}
public func renderRgb(layer: MetalImageLayer, width: Int, height: Int, bytesPerRow: Int, data: Data, completion: @escaping () -> Void) {
self.updateRgbTexture(width: width, height: height, bytesPerRow: bytesPerRow, data: data)
guard let rgbTexture = self.rgbTexture else {
return
}
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
return
}
commandBuffer.label = "MyCommand"
let drawableSize = CGSize(width: CGFloat(rgbTexture.width), height: CGFloat(rgbTexture.height))
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
commandBuffer.commit()
completion()
return
}
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
renderEncoder.label = "MyRenderEncoder"
renderEncoder.setRenderPipelineState(self.shared.renderRgbPipelineState)
renderEncoder.setFragmentTexture(rgbTexture.texture, index: 0)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
renderEncoder.endEncoding()
var storedDrawable: MetalImageLayer.Drawable? = drawable
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {
autoreleasepool {
storedDrawable?.present(completion: completion)
storedDrawable = nil
}
}
}
commandBuffer.commit()
}
private func updateYuvaTextures(width: Int, height: Int, data: Data) {
if width % 2 != 0 || height % 2 != 0 {
return
}
self.compressedTextures = nil
self.outputTextures = nil
self.rgbTexture = nil
let yuvaTextures: TextureSet
if let current = self.yuvaTextures, current.width == width, current.height == height {
yuvaTextures = current
} else {
guard let textures = TextureSet(
device: self.sharedContext.device,
width: width,
height: height,
descriptions: [
TextureSet.Description(
fractionWidth: 1, fractionHeight: 1,
pixelFormat: .r8Unorm
),
TextureSet.Description(
fractionWidth: 2, fractionHeight: 2,
pixelFormat: .rg8Unorm
),
TextureSet.Description(
fractionWidth: 2, fractionHeight: 1,
pixelFormat: .r8Uint
)
],
usage: .shaderRead,
isShared: true
) else {
return
}
self.yuvaTextures = textures
yuvaTextures = textures
}
data.withUnsafeBytes { yuvaBuffer in
guard let yuva = yuvaBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
yuvaTextures.textures[0].readDirect(width: width, height: height, bytesPerRow: width, read: { destinationBytes in
if let destinationBytes = destinationBytes {
memcpy(destinationBytes, yuva.advanced(by: 0), width * height)
return UnsafeRawPointer(destinationBytes)
} else {
return UnsafeRawPointer(yuva.advanced(by: 0))
}
})
yuvaTextures.textures[1].readDirect(width: width / 2, height: height / 2, bytesPerRow: width, read: { destinationBytes in
if let destinationBytes = destinationBytes {
memcpy(destinationBytes, yuva.advanced(by: width * height), width * height / 2)
return UnsafeRawPointer(destinationBytes)
} else {
return UnsafeRawPointer(yuva.advanced(by: width * height))
}
})
yuvaTextures.textures[2].readDirect(width: width / 2, height: height, bytesPerRow: width / 2, read: { destinationBytes in
if let destinationBytes = destinationBytes {
memcpy(destinationBytes, yuva.advanced(by: width * height * 2), width / 2 * height)
return UnsafeRawPointer(destinationBytes)
} else {
return UnsafeRawPointer(yuva.advanced(by: width * height * 2))
}
})
}
}
public func renderYuva(layer: MetalImageLayer, width: Int, height: Int, data: Data, completion: @escaping () -> Void) {
DispatchQueue.global().async {
autoreleasepool {
//let renderStartTime = CFAbsoluteTimeGetCurrent()
var beginTime: Double = 0.0
var duration: Double = 0.0
beginTime = CFAbsoluteTimeGetCurrent()
self.updateYuvaTextures(width: width, height: height, data: data)
duration = CFAbsoluteTimeGetCurrent() - beginTime
if duration > 1.0 / 60.0 {
print("update textures lag \(duration * 1000.0)")
}
guard let yuvaTextures = self.yuvaTextures else {
DispatchQueue.main.async {
completion()
}
return
}
beginTime = CFAbsoluteTimeGetCurrent()
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
DispatchQueue.main.async {
completion()
}
return
}
commandBuffer.label = "MyCommand"
let drawableSize = CGSize(width: CGFloat(yuvaTextures.width), height: CGFloat(yuvaTextures.height))
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
commandBuffer.commit()
DispatchQueue.main.async {
completion()
}
return
}
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
DispatchQueue.main.async {
completion()
}
return
}
renderEncoder.label = "MyRenderEncoder"
renderEncoder.setRenderPipelineState(self.shared.renderYuvaPipelineState)
renderEncoder.setFragmentTexture(yuvaTextures.textures[0].texture, index: 0)
renderEncoder.setFragmentTexture(yuvaTextures.textures[1].texture, index: 1)
renderEncoder.setFragmentTexture(yuvaTextures.textures[2].texture, index: 2)
var alphaSize = simd_uint2(UInt32(yuvaTextures.textures[0].texture.width), UInt32(yuvaTextures.textures[0].texture.height))
renderEncoder.setFragmentBytes(&alphaSize, length: 8, index: 3)
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
renderEncoder.endEncoding()
var storedDrawable: MetalImageLayer.Drawable? = drawable
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {
autoreleasepool {
storedDrawable?.present(completion: completion)
storedDrawable = nil
}
}
}
commandBuffer.commit()
duration = CFAbsoluteTimeGetCurrent() - beginTime
if duration > 1.0 / 60.0 {
print("commit lag \(duration * 1000.0)")
}
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimationUI",
module_name = "AnimationUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/rlottie:RLottieBinding",
"//submodules/lottie-ios:Lottie",
"//submodules/GZip:GZip",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,276 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Lottie
import GZip
import AppBundle
import Display
public final class AnimationNode: ASDisplayNode {
private let scale: CGFloat
public var speed: CGFloat = 1.0 {
didSet {
if let animationView = animationView() {
animationView.animationSpeed = speed
}
}
}
public var didPlay = false
public var completion: (() -> Void)?
private var internalCompletion: (() -> Void)?
public var isPlaying: Bool {
return self.animationView()?.isAnimationPlaying ?? false
}
private var currentParams: (String?, [String: UIColor]?)?
public init(animation animationName: String? = nil, colors: [String: UIColor]? = nil, scale: CGFloat = 1.0) {
self.scale = scale
self.currentParams = (animationName, colors)
super.init()
self.setViewBlock({ [weak self] in
guard let self else {
return UIView()
}
var animation: Animation?
if let animationName {
if let url = getAppBundle().url(forResource: animationName, withExtension: "json"), let maybeAnimation = Animation.filepath(url.path) {
animation = maybeAnimation
} else if let url = getAppBundle().url(forResource: animationName, withExtension: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
animation = try? Animation.from(data: unpackedData, strategy: .codable)
}
}
if let animation {
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = self.speed
view.backgroundColor = .clear
view.isOpaque = false
if let colors = colors {
for (key, value) in colors {
view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
}
if let value = colors["__allcolors__"] {
for keypath in view.allKeypaths(predicate: { $0.keys.last == "Color" }) {
view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
}
return view
} else {
return AnimationView()
}
})
}
public init(animationData: Data, colors: [String: UIColor]? = nil, scale: CGFloat = 1.0) {
self.scale = scale
super.init()
self.setViewBlock({
if let json = try? JSONSerialization.jsonObject(with: animationData, options: []) as? [String: Any], let animation = try? Animation(dictionary: json) {
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
view.animationSpeed = self.speed
view.backgroundColor = .clear
view.isOpaque = false
if let colors = colors {
for (key, value) in colors {
view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
}
if let value = colors["__allcolors__"] {
for keypath in view.allKeypaths(predicate: { $0.keys.last == "Color" }) {
view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
}
return view
} else {
return AnimationView()
}
})
}
public func makeCopy(colors: [String: UIColor]? = nil, progress: CGFloat? = nil) -> AnimationNode? {
guard let (animation, currentColors) = self.currentParams else {
return nil
}
let animationNode = AnimationNode(animation: animation, colors: colors ?? currentColors, scale: 1.0)
animationNode.animationView()?.currentProgress = progress ?? (self.animationView()?.currentProgress ?? 0.0)
animationNode.animationView()?.play(completion: { [weak animationNode] _ in
animationNode?.completion?()
})
return animationNode
}
public func seekToEnd() {
self.animationView()?.currentProgress = 1.0
}
public func setProgress(_ progress: CGFloat) {
self.animationView()?.currentProgress = progress
}
public func animate(from: CGFloat, to: CGFloat, completion: @escaping () -> Void) {
self.animationView()?.play(fromProgress: from, toProgress: to, completion: { _ in
completion()
})
}
public func setAnimation(name: String, colors: [String: UIColor]? = nil) {
self.currentParams = (name, colors)
if let url = getAppBundle().url(forResource: name, withExtension: "json"), let animation = Animation.filepath(url.path) {
self.didPlay = false
self.animationView()?.animation = animation
if let colors = colors {
for (key, value) in colors {
self.animationView()?.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
}
}
}
}
public func setColors(colors: [String: UIColor]) {
for (key, value) in colors {
self.animationView()?.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
}
}
public func setAnimation(data: Data) {
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
let animation = try? Animation(dictionary: json)
self.didPlay = false
self.animationView()?.animation = animation
}
}
public func setAnimation(json: [String: Any]) {
self.didPlay = false
if let animation = try? Animation(dictionary: json) {
self.animationView()?.animation = animation
}
}
public func animationView() -> AnimationView? {
return self.view as? AnimationView
}
public func play() {
if let animationView = self.animationView(), !animationView.isAnimationPlaying && !self.didPlay {
self.didPlay = true
animationView.play { [weak self] _ in
self?.completion?()
}
}
}
public func playOnce() {
if let animationView = self.animationView(), !animationView.isAnimationPlaying && !self.didPlay {
self.didPlay = true
self.internalCompletion = { [weak self] in
self?.didPlay = false
}
animationView.play { [weak self] _ in
self?.internalCompletion?()
}
}
}
public func loop(count: Int? = nil) {
if let animationView = self.animationView() {
if let count = count {
animationView.loopMode = .repeat(Float(count))
} else {
animationView.loopMode = .loop
}
animationView.play()
}
}
public func reset() {
if self.didPlay, let animationView = animationView() {
self.didPlay = false
animationView.stop()
}
}
public func preferredSize() -> CGSize? {
if let animationView = self.animationView(), let animation = animationView.animation {
return CGSize(width: animation.size.width * self.scale, height: animation.size.height * self.scale)
} else {
return nil
}
}
}
private let colorKeyRegex = try? NSRegularExpression(pattern: "\"k\":\\[[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\]")
public func transformedWithColors(data: Data, colors: [(UIColor, UIColor)]) -> Data {
if var string = String(data: data, encoding: .utf8) {
let sourceColors: [UIColor] = colors.map { $0.0 }
let replacementColors: [UIColor] = colors.map { $0.1 }
func colorToString(_ color: UIColor) -> String {
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
if color.getRed(&r, green: &g, blue: &b, alpha: nil) {
return "\"k\":[\(r),\(g),\(b),1]"
}
return ""
}
func match(_ a: Double, _ b: Double, eps: Double) -> Bool {
return abs(a - b) < eps
}
var replacements: [(NSTextCheckingResult, String)] = []
if let colorKeyRegex = colorKeyRegex {
let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string))
for result in results.reversed() {
if let range = Range(result.range, in: string) {
let substring = String(string[range])
let color = substring[substring.index(string.startIndex, offsetBy: "\"k\":[".count) ..< substring.index(before: substring.endIndex)]
let components = color.split(separator: ",")
if components.count == 4, let r = Double(components[0]), let g = Double(components[1]), let b = Double(components[2]), let a = Double(components[3]) {
if match(a, 1.0, eps: 0.01) {
for i in 0 ..< sourceColors.count {
let color = sourceColors[i]
var cr: CGFloat = 0.0
var cg: CGFloat = 0.0
var cb: CGFloat = 0.0
if color.getRed(&cr, green: &cg, blue: &cb, alpha: nil) {
if match(r, Double(cr), eps: 0.01) && match(g, Double(cg), eps: 0.01) && match(b, Double(cb), eps: 0.01) {
replacements.append((result, colorToString(replacementColors[i])))
}
}
}
}
}
}
}
}
for (result, text) in replacements {
if let range = Range(result.range, in: string) {
string = string.replacingCharacters(in: range, with: text)
}
}
return string.data(using: .utf8) ?? data
} else {
return data
}
}
+23
View File
@@ -0,0 +1,23 @@
objc_library(
name = "AppBundle",
module_name = "AppBundle",
enable_modules = True,
srcs = glob([
"Sources/**/*.m",
"Sources/**/*.h",
], allow_empty=True),
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
],
sdk_frameworks = [
"Foundation",
"UIKit",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NSBundle * _Nonnull getAppBundle(void);
@interface UIImage (AppBundle)
- (instancetype _Nullable)initWithBundleImageName:(NSString * _Nonnull)bundleImageName;
@end
@@ -0,0 +1,27 @@
#import <AppBundle/AppBundle.h>
NSBundle * _Nonnull getAppBundle() {
static NSBundle *appBundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSBundle *bundle = [NSBundle mainBundle];
if ([[bundle.bundleURL pathExtension] isEqualToString:@"appex"]) {
bundle = [NSBundle bundleWithURL:[[bundle.bundleURL URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]];
} else if ([[bundle.bundleURL pathExtension] isEqualToString:@"framework"]) {
bundle = [NSBundle bundleWithURL:[[bundle.bundleURL URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]];
} else if ([[bundle.bundleURL pathExtension] isEqualToString:@"Frameworks"]) {
bundle = [NSBundle bundleWithURL:[bundle.bundleURL URLByDeletingLastPathComponent]];
}
appBundle = bundle;
});
return appBundle;
}
@implementation UIImage (AppBundle)
- (instancetype _Nullable)initWithBundleImageName:(NSString * _Nonnull)bundleImageName {
return [UIImage imageNamed:bundleImageName inBundle:getAppBundle() compatibleWithTraitCollection:nil];
}
@end
+28
View File
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AppLock",
module_name = "AppLock",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/MonotonicTime:MonotonicTime",
"//submodules/PasscodeUI:PasscodeUI",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/ImageBlur:ImageBlur",
"//submodules/AccountContext:AccountContext",
"//submodules/AppLockState:AppLockState",
],
visibility = [
"//visibility:public",
],
)
+386
View File
@@ -0,0 +1,386 @@
import Foundation
import UIKit
import TelegramCore
import Display
import SwiftSignalKit
import MonotonicTime
import AccountContext
import TelegramPresentationData
import PasscodeUI
import TelegramUIPreferences
import ImageBlur
import FastBlur
import AppLockState
import PassKit
private func isLocked(passcodeSettings: PresentationPasscodeSettings, state: LockState, isApplicationActive: Bool) -> Bool {
if state.isManuallyLocked {
return true
} else if let autolockTimeout = passcodeSettings.autolockTimeout {
var bootTimestamp: Int32 = 0
let uptime = getDeviceUptimeSeconds(&bootTimestamp)
let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime)
let applicationActivityTimestamp = state.applicationActivityTimestamp
if let applicationActivityTimestamp = applicationActivityTimestamp {
if timestamp.bootTimestamp != applicationActivityTimestamp.bootTimestamp {
return true
}
if timestamp.uptime >= applicationActivityTimestamp.uptime + autolockTimeout {
return true
}
} else {
return true
}
}
return false
}
private func getCoveringViewSnaphot(window: Window1) -> UIImage? {
let scale: CGFloat = 0.5
let unscaledSize = window.hostView.containerView.frame.size
return generateImage(CGSize(width: floor(unscaledSize.width * scale), height: floor(unscaledSize.height * scale)), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.scaleBy(x: scale, y: scale)
UIGraphicsPushContext(context)
window.badgeView.alpha = 0.0
window.forEachViewController({ controller in
if let controller = controller as? PasscodeEntryController {
controller.displayNode.alpha = 0.0
}
return true
})
window.hostView.containerView.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false)
window.forEachViewController({ controller in
if let controller = controller as? PasscodeEntryController {
controller.displayNode.alpha = 1.0
}
return true
})
window.badgeView.alpha = 1.0
UIGraphicsPopContext()
}).flatMap(applyScreenshotEffectToImage)
}
public final class AppLockContextImpl: AppLockContext {
private let rootPath: String
private let syncQueue = Queue()
private var disposable: Disposable?
private var autolockTimeoutDisposable: Disposable?
private let applicationBindings: TelegramApplicationBindings
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private let presentationDataSignal: Signal<PresentationData, NoError>
private let window: Window1?
private let rootController: UIViewController?
private var coveringView: LockedWindowCoveringView?
private var passcodeController: PasscodeEntryController?
private var timestampRenewTimer: SwiftSignalKit.Timer?
private var currentStateValue: LockState
private let currentState = Promise<LockState>()
private let autolockTimeout = ValuePromise<Int32?>(nil, ignoreRepeated: true)
private let autolockReportTimeout = ValuePromise<Int32?>(nil, ignoreRepeated: true)
private let isCurrentlyLockedPromise = Promise<Bool>()
public var isCurrentlyLocked: Signal<Bool, NoError> {
return self.isCurrentlyLockedPromise.get()
|> distinctUntilChanged
}
private var lastActiveTimestamp: Double?
private var lastActiveValue: Bool = false
public init(rootPath: String, window: Window1?, rootController: UIViewController?, applicationBindings: TelegramApplicationBindings, accountManager: AccountManager<TelegramAccountManagerTypes>, presentationDataSignal: Signal<PresentationData, NoError>, lockIconInitialFrame: @escaping () -> CGRect?) {
assert(Queue.mainQueue().isCurrent())
self.applicationBindings = applicationBindings
self.accountManager = accountManager
self.presentationDataSignal = presentationDataSignal
self.rootPath = rootPath
self.window = window
self.rootController = rootController
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: self.rootPath))), let current = try? JSONDecoder().decode(LockState.self, from: data) {
self.currentStateValue = current
} else {
self.currentStateValue = LockState()
}
self.autolockTimeout.set(self.currentStateValue.autolockTimeout)
self.disposable = (combineLatest(queue: .mainQueue(),
accountManager.accessChallengeData(),
accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.presentationPasscodeSettings])),
presentationDataSignal,
applicationBindings.applicationIsActive,
self.currentState.get()
)
|> deliverOnMainQueue).startStrict(next: { [weak self] accessChallengeData, sharedData, presentationData, appInForeground, state in
guard let strongSelf = self else {
return
}
let passcodeSettings: PresentationPasscodeSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]?.get(PresentationPasscodeSettings.self) ?? .defaultSettings
let timestamp = CFAbsoluteTimeGetCurrent()
var becameActiveRecently = false
if appInForeground {
if !strongSelf.lastActiveValue {
strongSelf.lastActiveValue = true
strongSelf.lastActiveTimestamp = timestamp
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: strongSelf.rootPath))), let current = try? JSONDecoder().decode(LockState.self, from: data) {
strongSelf.currentStateValue = current
}
}
if let lastActiveTimestamp = strongSelf.lastActiveTimestamp {
if lastActiveTimestamp + 0.5 > timestamp {
becameActiveRecently = true
}
}
} else {
strongSelf.lastActiveValue = false
}
var shouldDisplayCoveringView = false
var isCurrentlyLocked = false
if !accessChallengeData.data.isLockable {
if let passcodeController = strongSelf.passcodeController {
strongSelf.passcodeController = nil
passcodeController.dismiss()
}
strongSelf.autolockTimeout.set(nil)
strongSelf.autolockReportTimeout.set(nil)
} else {
if let _ = passcodeSettings.autolockTimeout, !appInForeground {
shouldDisplayCoveringView = true
}
if !appInForeground {
if let autolockTimeout = passcodeSettings.autolockTimeout {
strongSelf.autolockReportTimeout.set(autolockTimeout)
} else if state.isManuallyLocked {
strongSelf.autolockReportTimeout.set(1)
} else {
strongSelf.autolockReportTimeout.set(nil)
}
} else {
strongSelf.autolockReportTimeout.set(nil)
}
strongSelf.autolockTimeout.set(passcodeSettings.autolockTimeout)
if isLocked(passcodeSettings: passcodeSettings, state: state, isApplicationActive: appInForeground) {
isCurrentlyLocked = true
let biometrics: PasscodeEntryControllerBiometricsMode
if passcodeSettings.enableBiometrics {
biometrics = .enabled(passcodeSettings.biometricsDomainState)
} else {
biometrics = .none
}
if let passcodeController = strongSelf.passcodeController {
if becameActiveRecently, case .enabled = biometrics, appInForeground {
passcodeController.requestBiometrics()
}
passcodeController.ensureInputFocused()
} else {
let passcodeController = PasscodeEntryController(applicationBindings: strongSelf.applicationBindings, accountManager: strongSelf.accountManager, appLockContext: strongSelf, presentationData: presentationData, presentationDataSignal: strongSelf.presentationDataSignal, statusBarHost: window?.statusBarHost, challengeData: accessChallengeData.data, biometrics: biometrics, arguments: PasscodeEntryControllerPresentationArguments(animated: !becameActiveRecently, lockIconInitialFrame: {
if let lockViewFrame = lockIconInitialFrame() {
return lockViewFrame
} else {
return CGRect()
}
}))
if becameActiveRecently, appInForeground {
passcodeController.presentationCompleted = { [weak passcodeController] in
if case .enabled = biometrics {
passcodeController?.requestBiometrics()
}
passcodeController?.ensureInputFocused()
}
}
passcodeController.presentedOverCoveringView = true
passcodeController.isOpaqueWhenInOverlay = true
strongSelf.passcodeController = passcodeController
if let rootViewController = strongSelf.rootController {
if let _ = rootViewController.presentedViewController as? UIActivityViewController {
} else if let _ = rootViewController.presentedViewController as? PKPaymentAuthorizationViewController {
} else {
rootViewController.dismiss(animated: false, completion: nil)
}
}
strongSelf.window?.present(passcodeController, on: .passcode)
}
} else if let passcodeController = strongSelf.passcodeController {
strongSelf.passcodeController = nil
passcodeController.dismiss()
}
}
strongSelf.updateTimestampRenewTimer(shouldRun: appInForeground && !isCurrentlyLocked)
strongSelf.isCurrentlyLockedPromise.set(.single(!appInForeground || isCurrentlyLocked))
if shouldDisplayCoveringView {
if strongSelf.coveringView == nil, let window = strongSelf.window {
let coveringView = LockedWindowCoveringView(theme: presentationData.theme)
coveringView.updateSnapshot(getCoveringViewSnaphot(window: window))
strongSelf.coveringView = coveringView
window.coveringView = coveringView
if let rootViewController = strongSelf.rootController {
if let _ = rootViewController.presentedViewController as? UIActivityViewController {
} else if let _ = rootViewController.presentedViewController as? PKPaymentAuthorizationViewController {
} else {
rootViewController.dismiss(animated: false, completion: nil)
}
}
}
} else {
if let _ = strongSelf.coveringView {
strongSelf.coveringView = nil
strongSelf.window?.coveringView = nil
}
}
})
self.currentState.set(.single(self.currentStateValue))
self.autolockTimeoutDisposable = (self.autolockTimeout.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] autolockTimeout in
self?.updateLockState { state in
var state = state
state.autolockTimeout = autolockTimeout
return state
}
})
}
deinit {
self.disposable?.dispose()
self.autolockTimeoutDisposable?.dispose()
}
private func updateTimestampRenewTimer(shouldRun: Bool) {
if shouldRun {
if self.timestampRenewTimer == nil {
let timestampRenewTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateApplicationActivityTimestamp()
}, queue: .mainQueue())
self.timestampRenewTimer = timestampRenewTimer
timestampRenewTimer.start()
}
} else {
if let timestampRenewTimer = self.timestampRenewTimer {
self.timestampRenewTimer = nil
timestampRenewTimer.invalidate()
}
}
}
private func updateApplicationActivityTimestamp() {
self.updateLockState { state in
var bootTimestamp: Int32 = 0
let uptime = getDeviceUptimeSeconds(&bootTimestamp)
var state = state
state.applicationActivityTimestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime)
return state
}
}
private func updateLockState(_ f: @escaping (LockState) -> LockState) {
Queue.mainQueue().async {
let updatedState = f(self.currentStateValue)
if updatedState != self.currentStateValue {
self.currentStateValue = updatedState
self.currentState.set(.single(updatedState))
let path = appLockStatePath(rootPath: self.rootPath)
self.syncQueue.async {
if let data = try? JSONEncoder().encode(updatedState) {
let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
}
}
}
}
}
public var invalidAttempts: Signal<AccessChallengeAttempts?, NoError> {
return self.currentState.get()
|> map { state in
return state.unlockAttempts.flatMap { unlockAttempts in
return AccessChallengeAttempts(count: unlockAttempts.count, bootTimestamp: unlockAttempts.timestamp.bootTimestamp, uptime: unlockAttempts.timestamp.uptime)
}
}
}
public var autolockDeadline: Signal<Int32?, NoError> {
return self.autolockReportTimeout.get()
|> distinctUntilChanged
|> map { value -> Int32? in
if let value = value {
return Int32(Date().timeIntervalSince1970) + value
} else {
return nil
}
}
}
public func lock() {
self.updateLockState { state in
var state = state
state.isManuallyLocked = true
return state
}
}
public func unlock() {
self.updateLockState { state in
var state = state
state.unlockAttempts = nil
state.isManuallyLocked = false
var bootTimestamp: Int32 = 0
let uptime = getDeviceUptimeSeconds(&bootTimestamp)
let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime)
state.applicationActivityTimestamp = timestamp
return state
}
}
public func failedUnlockAttempt() {
self.updateLockState { state in
var state = state
var unlockAttempts = state.unlockAttempts ?? UnlockAttempts(count: 0, timestamp: MonotonicTimestamp(bootTimestamp: 0, uptime: 0))
unlockAttempts.count += 1
var bootTimestamp: Int32 = 0
let uptime = getDeviceUptimeSeconds(&bootTimestamp)
let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime)
unlockAttempts.timestamp = timestamp
state.unlockAttempts = unlockAttempts
return state
}
}
}
@@ -0,0 +1,36 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import AsyncDisplayKit
final class LockedWindowCoveringView: WindowCoveringView {
private let contentView: UIImageView
init(theme: PresentationTheme) {
self.contentView = UIImageView()
super.init(frame: CGRect())
self.backgroundColor = theme.chatList.backgroundColor
self.addSubview(self.contentView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateTheme(_ theme: PresentationTheme) {
self.backgroundColor = theme.chatList.backgroundColor
}
func updateSnapshot(_ image: UIImage?) {
if image != nil {
self.contentView.image = image
}
}
override func updateLayout(_ size: CGSize) {
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
}
}
+15
View File
@@ -0,0 +1,15 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AppLockState",
module_name = "AppLockState",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/MonotonicTime:MonotonicTime",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,62 @@
import Foundation
import MonotonicTime
public struct MonotonicTimestamp: Codable, Equatable {
public var bootTimestamp: Int32
public var uptime: Int32
public init(bootTimestamp: Int32, uptime: Int32) {
self.bootTimestamp = bootTimestamp
self.uptime = uptime
}
}
public struct UnlockAttempts: Codable, Equatable {
public var count: Int32
public var timestamp: MonotonicTimestamp
public init(count: Int32, timestamp: MonotonicTimestamp) {
self.count = count
self.timestamp = timestamp
}
}
public struct LockState: Codable, Equatable {
public var isManuallyLocked: Bool
public var autolockTimeout: Int32?
public var unlockAttempts: UnlockAttempts?
public var applicationActivityTimestamp: MonotonicTimestamp?
public init(isManuallyLocked: Bool = false, autolockTimeout: Int32? = nil, unlockAttemts: UnlockAttempts? = nil, applicationActivityTimestamp: MonotonicTimestamp? = nil) {
self.isManuallyLocked = isManuallyLocked
self.autolockTimeout = autolockTimeout
self.unlockAttempts = unlockAttemts
self.applicationActivityTimestamp = applicationActivityTimestamp
}
}
public func appLockStatePath(rootPath: String) -> String {
return rootPath + "/lockState.json"
}
public func isAppLocked(state: LockState) -> Bool {
if state.isManuallyLocked {
return true
} else if let autolockTimeout = state.autolockTimeout {
var bootTimestamp: Int32 = 0
let uptime = getDeviceUptimeSeconds(&bootTimestamp)
let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime)
if let applicationActivityTimestamp = state.applicationActivityTimestamp {
if timestamp.bootTimestamp != applicationActivityTimestamp.bootTimestamp {
return true
}
if timestamp.uptime >= applicationActivityTimestamp.uptime + autolockTimeout {
return true
}
} else {
return true
}
}
return false
}
@@ -0,0 +1,30 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ArchivedStickerPacksNotice",
module_name = "ArchivedStickerPacksNotice",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/StickerResources:StickerResources",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/MergeLists:MergeLists",
"//submodules/ItemListUI:ItemListUI",
"//submodules/ItemListStickerPackItem:ItemListStickerPackItem",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,323 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ActivityIndicator
import AccountContext
import AlertUI
import PresentationDataUtils
import MergeLists
import ItemListUI
import ItemListStickerPackItem
private struct ArchivedStickersNoticeEntry: Comparable, Identifiable {
let index: Int
let info: StickerPackCollectionInfo.Accessor
let topItem: StickerPackItem?
let count: String
var stableId: AnyHashable {
return AnyHashable(self.info.id)
}
static func ==(lhs: ArchivedStickersNoticeEntry, rhs: ArchivedStickersNoticeEntry) -> Bool {
return lhs.index == rhs.index && lhs.info.id == rhs.info.id && lhs.count == rhs.count
}
static func <(lhs: ArchivedStickersNoticeEntry, rhs: ArchivedStickersNoticeEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData) -> ListViewItem {
return ItemListStickerPackItem(presentationData: ItemListPresentationData(presentationData), context: context, packInfo: info, itemCount: self.count, topItem: topItem, unread: false, control: .none, editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false, selectable: false), enabled: true, playAnimatedStickers: true, sectionId: 0, action: {
}, setPackIdWithRevealedOptions: { current, previous in
}, addPack: {
}, removePack: {
}, toggleSelected: {
})
}
}
private struct ArchivedStickersNoticeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private func preparedTransition(from fromEntries: [ArchivedStickersNoticeEntry], to toEntries: [ArchivedStickersNoticeEntry], context: AccountContext, presentationData: PresentationData) -> ArchivedStickersNoticeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData), directionHint: nil) }
return ArchivedStickersNoticeTransition(deletions: deletions, insertions: insertions, updates: updates)
}
private final class ArchivedStickersNoticeAlertContentNode: AlertContentNode {
private let presentationData: PresentationData
private let archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)]
private let textNode: ASTextNode
private let listView: ListView
private var enqueuedTransitions: [ArchivedStickersNoticeTransition] = []
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, context: AccountContext, presentationData: PresentationData, archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)], actions: [TextAlertAction]) {
self.presentationData = presentationData
self.archivedStickerPacks = archivedStickerPacks
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 4
self.listView = ListView()
self.listView.isOpaque = false
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.listView)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = false
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
var index: Int = 0
var entries: [ArchivedStickersNoticeEntry] = []
for pack in archivedStickerPacks {
let countTitle: String
if pack.0.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks {
countTitle = presentationData.strings.StickerPack_EmojiCount(pack.0.count)
} else if pack.0.id.namespace == Namespaces.ItemCollection.CloudMaskPacks {
countTitle = presentationData.strings.StickerPack_MaskCount(pack.0.count)
} else {
countTitle = presentationData.strings.StickerPack_StickerCount(pack.0.count)
}
entries.append(ArchivedStickersNoticeEntry(index: index, info: StickerPackCollectionInfo.Accessor(pack.0), topItem: pack.1, count: countTitle))
index += 1
}
let transition = preparedTransition(from: [], to: entries, context: context, presentationData: presentationData)
self.enqueueTransition(transition)
}
deinit {
self.disposable.dispose()
}
private func enqueueTransition(_ transition: ArchivedStickersNoticeTransition) {
self.enqueuedTransitions.append(transition)
if let _ = self.validLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
let options = ListViewDeleteAndInsertOptions()
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.ArchivedPacksAlert_Title, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 16.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var contentWidth = max(textSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let listHeight: CGFloat = CGFloat(min(3, self.archivedStickerPacks.count)) * 56.0
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: resultWidth, height: listHeight), insets: UIEdgeInsets(top: -35.0, left: 0.0, bottom: 0.0, right: 0.0), headerInsets: UIEdgeInsets(), scrollIndicatorInsets: UIEdgeInsets(), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: listHeight))
let resultSize = CGSize(width: resultWidth, height: textSize.height + actionsHeight + listHeight + 10.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if !hadValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
return resultSize
}
}
public func archivedStickerPacksNoticeController(context: AccountContext, archivedStickerPacks: [(StickerPackCollectionInfo, StickerPackItem?)]) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: (() -> Void)?
let disposable = MetaDisposable()
let contentNode = ArchivedStickersNoticeAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), context: context, presentationData: presentationData, archivedStickerPacks: archivedStickerPacks, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
dismissImpl?()
})])
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
disposable.dispose()
}
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
return controller
}
+25
View File
@@ -0,0 +1,25 @@
fastlane/README.md
fastlane/report.xml
fastlane/test_output/*
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.xcscmblueprint
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
.DS_Store
*.dSYM
*.dSYM.zip
*.ipa
*/xcuserdata/*
AsyncDisplayKit.xcodeproj/*
+42
View File
@@ -0,0 +1,42 @@
public_headers = glob([
"Source/PublicHeaders/AsyncDisplayKit/*.h",
])
private_headers = glob([
"Source/*.h",
], allow_empty=True)
objc_library(
name = "AsyncDisplayKit",
enable_modules = True,
module_name = "AsyncDisplayKit",
srcs = glob([
"Source/**/*.m",
"Source/**/*.mm",
], allow_empty=True) + private_headers,
copts = [
"-Werror",
],
cxxopts = [
"-Werror",
"-std=c++17",
],
hdrs = public_headers,
defines = [
"MINIMAL_ASDK",
],
includes = [
"Source/PublicHeaders",
],
sdk_frameworks = [
"Foundation",
"UIKit",
"QuartzCore",
"CoreMedia",
"CoreText",
"CoreGraphics",
],
visibility = [
"//visibility:public",
],
)
+181
View File
@@ -0,0 +1,181 @@
The Texture project was created by Pinterest as a continuation, under a different
name and license, of the AsyncDisplayKit codebase originally developed by Facebook.
All code in Texture is covered by the Apache License, Version 2.0.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
@@ -0,0 +1,21 @@
//
// ASAbstractLayoutController+FrameworkPrivate.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
//
// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode.
// These methods must never be called or overridden by other classes.
//
#include <vector>
@interface ASAbstractLayoutController (FrameworkPrivate)
+ (std::vector<std::vector<ASRangeTuningParameters>>)defaultTuningParameters;
@end
@@ -0,0 +1,186 @@
//
// ASAsciiArtBoxCreator.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASAsciiArtBoxCreator.h>
#import <CoreGraphics/CoreGraphics.h>
#import <cmath>
static const NSUInteger kDebugBoxPadding = 2;
typedef NS_ENUM(NSUInteger, PIDebugBoxPaddingLocation)
{
PIDebugBoxPaddingLocationFront,
PIDebugBoxPaddingLocationEnd,
PIDebugBoxPaddingLocationBoth
};
@interface NSString(PIDebugBox)
@end
@implementation NSString(PIDebugBox)
+ (instancetype)debugbox_stringWithString:(NSString *)stringToRepeat repeatedCount:(NSUInteger)repeatCount NS_RETURNS_RETAINED
{
NSMutableString *string = [[NSMutableString alloc] initWithCapacity:[stringToRepeat length] * repeatCount];
for (NSUInteger index = 0; index < repeatCount; index++) {
[string appendString:stringToRepeat];
}
return [string copy];
}
- (NSString *)debugbox_stringByAddingPadding:(NSString *)padding count:(NSUInteger)count location:(PIDebugBoxPaddingLocation)location
{
NSString *paddingString = [NSString debugbox_stringWithString:padding repeatedCount:count];
switch (location) {
case PIDebugBoxPaddingLocationFront:
return [NSString stringWithFormat:@"%@%@", paddingString, self];
case PIDebugBoxPaddingLocationEnd:
return [NSString stringWithFormat:@"%@%@", self, paddingString];
case PIDebugBoxPaddingLocationBoth:
return [NSString stringWithFormat:@"%@%@%@", paddingString, self, paddingString];
}
return [self copy];
}
@end
@implementation ASAsciiArtBoxCreator
+ (NSString *)horizontalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent
{
if ([children count] == 0) {
return parent;
}
NSMutableArray *childrenLines = [NSMutableArray array];
// split the children into lines
NSUInteger lineCountPerChild = 0;
for (NSString *child in children) {
NSArray *lines = [child componentsSeparatedByString:@"\n"];
lineCountPerChild = MAX(lineCountPerChild, [lines count]);
}
for (NSString *child in children) {
NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy];
NSUInteger topPadding = ceil((CGFloat)(lineCountPerChild - [lines count])/2.0);
NSUInteger bottomPadding = (lineCountPerChild - [lines count])/2.0;
NSUInteger lineLength = [lines[0] length];
for (NSUInteger index = 0; index < topPadding; index++) {
[lines insertObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength] atIndex:0];
}
for (NSUInteger index = 0; index < bottomPadding; index++) {
[lines addObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength]];
}
[childrenLines addObject:lines];
}
NSMutableArray *concatenatedLines = [NSMutableArray array];
NSString *padding = [NSString debugbox_stringWithString:@" " repeatedCount:kDebugBoxPadding];
for (NSUInteger index = 0; index < lineCountPerChild; index++) {
NSMutableString *line = [[NSMutableString alloc] init];
[line appendFormat:@"|%@",padding];
for (NSArray *childLines in childrenLines) {
[line appendFormat:@"%@%@", childLines[index], padding];
}
[line appendString:@"|"];
[concatenatedLines addObject:line];
}
// surround the lines in a box
NSUInteger totalLineLength = [concatenatedLines[0] length];
if (totalLineLength < [parent length]) {
NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - totalLineLength;
NSUInteger leftPadding = ceil((CGFloat)difference/2.0);
NSUInteger rightPadding = difference/2;
NSString *leftString = [@"|" debugbox_stringByAddingPadding:@" " count:leftPadding location:PIDebugBoxPaddingLocationEnd];
NSString *rightString = [@"|" debugbox_stringByAddingPadding:@" " count:rightPadding location:PIDebugBoxPaddingLocationFront];
NSMutableArray *paddedLines = [NSMutableArray array];
for (NSString *line in concatenatedLines) {
NSString *paddedLine = [line stringByReplacingOccurrencesOfString:@"|" withString:leftString options:NSCaseInsensitiveSearch range:NSMakeRange(0, 1)];
paddedLine = [paddedLine stringByReplacingOccurrencesOfString:@"|" withString:rightString options:NSCaseInsensitiveSearch range:NSMakeRange([paddedLine length] - 1, 1)];
[paddedLines addObject:paddedLine];
}
concatenatedLines = paddedLines;
// totalLineLength += difference;
}
concatenatedLines = [self appendTopAndBottomToBoxString:concatenatedLines parent:parent];
return [concatenatedLines componentsJoinedByString:@"\n"];
}
+ (NSString *)verticalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent
{
if ([children count] == 0) {
return parent;
}
NSMutableArray *childrenLines = [NSMutableArray array];
NSUInteger maxChildLength = 0;
for (NSString *child in children) {
NSArray *lines = [child componentsSeparatedByString:@"\n"];
maxChildLength = MAX(maxChildLength, [lines[0] length]);
}
NSUInteger rightPadding = 0;
NSUInteger leftPadding = 0;
if (maxChildLength < [parent length]) {
NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - maxChildLength;
leftPadding = ceil((CGFloat)difference/2.0);
rightPadding = difference/2;
}
NSString *rightPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightPadding + kDebugBoxPadding];
NSString *leftPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftPadding + kDebugBoxPadding];
for (NSString *child in children) {
NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy];
NSUInteger leftLinePadding = ceil((CGFloat)(maxChildLength - [lines[0] length])/2.0);
NSUInteger rightLinePadding = (maxChildLength - [lines[0] length])/2.0;
for (NSString *line in lines) {
NSString *rightLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightLinePadding];
rightLinePaddingString = [NSString stringWithFormat:@"%@%@|", rightLinePaddingString, rightPaddingString];
NSString *leftLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftLinePadding];
leftLinePaddingString = [NSString stringWithFormat:@"|%@%@", leftLinePaddingString, leftPaddingString];
NSString *paddingLine = [NSString stringWithFormat:@"%@%@%@", leftLinePaddingString, line, rightLinePaddingString];
[childrenLines addObject:paddingLine];
}
}
childrenLines = [self appendTopAndBottomToBoxString:childrenLines parent:parent];
return [childrenLines componentsJoinedByString:@"\n"];
}
+ (NSMutableArray *)appendTopAndBottomToBoxString:(NSMutableArray *)boxStrings parent:(NSString *)parent
{
NSUInteger totalLineLength = [boxStrings[0] length];
[boxStrings addObject:[NSString debugbox_stringWithString:@"-" repeatedCount:totalLineLength]];
NSUInteger leftPadding = ceil(((CGFloat)(totalLineLength - [parent length]))/2.0);
NSUInteger rightPadding = (totalLineLength - [parent length])/2;
NSString *topLine = [parent debugbox_stringByAddingPadding:@"-" count:leftPadding location:PIDebugBoxPaddingLocationFront];
topLine = [topLine debugbox_stringByAddingPadding:@"-" count:rightPadding location:PIDebugBoxPaddingLocationEnd];
[boxStrings insertObject:topLine atIndex:0];
return boxStrings;
}
@end
@@ -0,0 +1,58 @@
//
// ASAssert.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASAvailability.h>
#if AS_TLS_AVAILABLE
static _Thread_local int tls_mainThreadAssertionsDisabledCount;
BOOL ASMainThreadAssertionsAreDisabled() {
return tls_mainThreadAssertionsDisabledCount > 0;
}
void ASPushMainThreadAssertionsDisabled() {
tls_mainThreadAssertionsDisabledCount += 1;
}
void ASPopMainThreadAssertionsDisabled() {
tls_mainThreadAssertionsDisabledCount -= 1;
ASDisplayNodeCAssert(tls_mainThreadAssertionsDisabledCount >= 0, @"Attempt to pop thread assertion-disabling without corresponding push.");
}
#else
#import <dispatch/once.h>
static pthread_key_t ASMainThreadAssertionsDisabledKey() {
static pthread_key_t k;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
pthread_key_create(&k, NULL);
});
return k;
}
BOOL ASMainThreadAssertionsAreDisabled() {
return (nullptr != pthread_getspecific(ASMainThreadAssertionsDisabledKey()));
}
void ASPushMainThreadAssertionsDisabled() {
const auto key = ASMainThreadAssertionsDisabledKey();
const auto oldVal = (intptr_t)pthread_getspecific(key);
pthread_setspecific(key, (void *)(oldVal + 1));
}
void ASPopMainThreadAssertionsDisabled() {
const auto key = ASMainThreadAssertionsDisabledKey();
const auto oldVal = (intptr_t)pthread_getspecific(key);
pthread_setspecific(key, (void *)(oldVal - 1));
ASDisplayNodeCAssert(oldVal > 0, @"Attempt to pop thread assertion-disabling without corresponding push.");
}
#endif // AS_TLS_AVAILABLE
@@ -0,0 +1,88 @@
//
// ASCGImageBuffer.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASCGImageBuffer.h>
#import <sys/mman.h>
#import <mach/mach_init.h>
#import <mach/vm_map.h>
#import <mach/vm_statistics.h>
/**
* The behavior of this class is modeled on the private function
* _CGDataProviderCreateWithCopyOfData, which is the function used
* by CGBitmapContextCreateImage.
*
* If the buffer is larger than a page, we use mmap and mark it as
* read-only when they are finished drawing. Then we wrap the VM
* in an NSData
*/
@implementation ASCGImageBuffer {
BOOL _createdData;
BOOL _isVM;
NSUInteger _length;
}
- (instancetype)initWithLength:(NSUInteger)length
{
if (self = [super init]) {
_length = length;
_isVM = false;//(length >= vm_page_size);
if (_isVM) {
_mutableBytes = mmap(NULL, length, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, VM_MAKE_TAG(VM_MEMORY_COREGRAPHICS_DATA), 0);
if (_mutableBytes == MAP_FAILED) {
NSAssert(NO, @"Failed to map for CG image data.");
_isVM = NO;
}
}
// Check the VM flag again because we may have failed above.
if (!_isVM) {
_mutableBytes = malloc(length);
}
}
return self;
}
- (void)dealloc
{
if (!_createdData) {
[ASCGImageBuffer deallocateBuffer:_mutableBytes length:_length isVM:_isVM];
}
}
- (CGDataProviderRef)createDataProviderAndInvalidate
{
NSAssert(!_createdData, @"Should not create data provider from buffer multiple times.");
_createdData = YES;
// Mark the pages as read-only.
if (_isVM) {
__unused kern_return_t result = vm_protect(mach_task_self(), (vm_address_t)_mutableBytes, _length, true, VM_PROT_READ);
NSAssert(result == noErr, @"Error marking buffer as read-only: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]);
}
// Wrap in an NSData
BOOL isVM = _isVM;
NSData *d = [[NSData alloc] initWithBytesNoCopy:_mutableBytes length:_length deallocator:^(void * _Nonnull bytes, NSUInteger length) {
[ASCGImageBuffer deallocateBuffer:bytes length:length isVM:isVM];
}];
return CGDataProviderCreateWithCFData((__bridge CFDataRef)d);
}
+ (void)deallocateBuffer:(void *)buf length:(NSUInteger)length isVM:(BOOL)isVM
{
if (isVM) {
__unused kern_return_t result = vm_deallocate(mach_task_self(), (vm_address_t)buf, length);
NSAssert(result == noErr, @"Failed to unmap cg image buffer: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]);
} else {
free(buf);
}
}
@end
@@ -0,0 +1,61 @@
//
// ASCollections.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASCollections.h>
/**
* A private allocator that signals to our retain callback to skip the retain.
* It behaves the same as the default allocator, but acts as a signal that we
* are creating a transfer array so we should skip the retain.
*/
static CFAllocatorRef gTransferAllocator;
static const void *ASTransferRetain(CFAllocatorRef allocator, const void *val) {
if (allocator == gTransferAllocator) {
// Transfer allocator. Ignore retain and pass through.
return val;
} else {
// Other allocator. Retain like normal.
// This happens when they make a mutable copy.
return (&kCFTypeArrayCallBacks)->retain(allocator, val);
}
}
@implementation NSArray (ASCollections)
+ (NSArray *)arrayByTransferring:(__strong id *)pointers count:(NSUInteger)count NS_RETURNS_RETAINED
{
// Custom callbacks that point to our ASTransferRetain callback.
static CFArrayCallBacks callbacks;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
callbacks = kCFTypeArrayCallBacks;
callbacks.retain = ASTransferRetain;
CFAllocatorContext ctx;
CFAllocatorGetContext(NULL, &ctx);
gTransferAllocator = CFAllocatorCreate(NULL, &ctx);
});
// NSZeroArray fast path.
if (count == 0) {
return @[]; // Does not actually call +array when optimized.
}
// NSSingleObjectArray fast path. Retain/release here is worth it.
if (count == 1) {
NSArray *result = [[NSArray alloc] initWithObjects:pointers count:1];
pointers[0] = nil;
return result;
}
NSArray *result = (__bridge_transfer NSArray *)CFArrayCreate(gTransferAllocator, (const void **)(void *)pointers, count, &callbacks);
memset(pointers, 0, count * sizeof(id));
return result;
}
@end
@@ -0,0 +1,64 @@
//
// ASConfiguration.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
/// Not too performance-sensitive here.
@implementation ASConfiguration
- (instancetype)initWithDictionary:(NSDictionary *)dictionary
{
if (self = [super init]) {
if (dictionary != nil) {
const auto featureStrings = ASDynamicCast(dictionary[@"experimental_features"], NSArray);
const auto version = ASDynamicCast(dictionary[@"version"], NSNumber).integerValue;
if (version != ASConfigurationSchemaCurrentVersion) {
NSLog(@"Texture warning: configuration schema is old version (%ld vs %ld)", (long)version, (long)ASConfigurationSchemaCurrentVersion);
}
self.experimentalFeatures = ASExperimentalFeaturesFromArray(featureStrings);
} else {
self.experimentalFeatures = kNilOptions;
}
}
return self;
}
- (id)copyWithZone:(NSZone *)zone
{
ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil];
config.experimentalFeatures = self.experimentalFeatures;
config.delegate = self.delegate;
return config;
}
@end
//#define AS_FIXED_CONFIG_JSON "{ \"version\" : 1, \"experimental_features\": [ \"exp_text_node\" ] }"
#ifdef AS_FIXED_CONFIG_JSON
@implementation ASConfiguration (UserProvided)
+ (ASConfiguration *)textureConfiguration NS_RETURNS_RETAINED
{
NSData *data = [@AS_FIXED_CONFIG_JSON dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSDictionary *d = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
if (!d) {
NSAssert(NO, @"Error parsing fixed config string '%s': %@", AS_FIXED_CONFIG_JSON, error);
return nil;
} else {
return [[ASConfiguration alloc] initWithDictionary:d];
}
}
@end
#endif // AS_FIXED_CONFIG_JSON
@@ -0,0 +1,111 @@
//
// ASConfigurationInternal.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationDelegate.h>
#import <stdatomic.h>
static ASConfigurationManager *ASSharedConfigurationManager;
static dispatch_once_t ASSharedConfigurationManagerOnceToken;
NS_INLINE ASConfigurationManager *ASConfigurationManagerGet() {
dispatch_once(&ASSharedConfigurationManagerOnceToken, ^{
ASSharedConfigurationManager = [[ASConfigurationManager alloc] init];
});
return ASSharedConfigurationManager;
}
@implementation ASConfigurationManager {
ASConfiguration *_config;
dispatch_queue_t _delegateQueue;
BOOL _frameworkInitialized;
_Atomic(ASExperimentalFeatures) _activatedExperiments;
}
+ (ASConfiguration *)defaultConfiguration NS_RETURNS_RETAINED
{
ASConfiguration *config = [[ASConfiguration alloc] init];
// TODO(wsdwsd0829): Fix #788 before enabling it.
// config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing;
return config;
}
- (instancetype)init
{
if (self = [super init]) {
_delegateQueue = dispatch_queue_create("org.TextureGroup.Texture.ConfigNotifyQueue", DISPATCH_QUEUE_SERIAL);
if ([ASConfiguration respondsToSelector:@selector(textureConfiguration)]) {
_config = [[ASConfiguration textureConfiguration] copy];
} else {
_config = [ASConfigurationManager defaultConfiguration];
}
}
return self;
}
- (void)frameworkDidInitialize
{
ASDisplayNodeAssertMainThread();
if (_frameworkInitialized) {
ASDisplayNodeFailAssert(@"Framework initialized twice.");
return;
}
_frameworkInitialized = YES;
const auto delegate = _config.delegate;
if ([delegate respondsToSelector:@selector(textureDidInitialize)]) {
[delegate textureDidInitialize];
}
}
- (BOOL)activateExperimentalFeature:(ASExperimentalFeatures)requested
{
if (_config == nil) {
return NO;
}
NSAssert(__builtin_popcountl(requested) == 1, @"Cannot activate multiple features at once with this method.");
// We need to call out, whether it's enabled or not.
// A/B testing requires even "control" users to be activated.
ASExperimentalFeatures enabled = requested & _config.experimentalFeatures;
ASExperimentalFeatures prevTriggered = atomic_fetch_or(&_activatedExperiments, requested);
ASExperimentalFeatures newlyTriggered = requested & ~prevTriggered;
// Notify delegate if needed.
if (newlyTriggered != 0) {
__unsafe_unretained id<ASConfigurationDelegate> del = _config.delegate;
dispatch_async(_delegateQueue, ^{
[del textureDidActivateExperimentalFeatures:newlyTriggered];
});
}
return (enabled != 0);
}
// Define this even when !DEBUG, since we may run our tests in release mode.
+ (void)test_resetWithConfiguration:(ASConfiguration *)configuration
{
ASConfigurationManager *inst = ASConfigurationManagerGet();
inst->_config = configuration ?: [self defaultConfiguration];
atomic_store(&inst->_activatedExperiments, 0);
}
@end
BOOL _ASActivateExperimentalFeature(ASExperimentalFeatures feature)
{
return [ASConfigurationManagerGet() activateExperimentalFeature:feature];
}
void ASNotifyInitialized()
{
[ASConfigurationManagerGet() frameworkDidInitialize];
}
@@ -0,0 +1,18 @@
//
// ASControlNode+Private.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASControlNode.h>
@interface ASControlNode (Private)
#if TARGET_OS_TV
- (void)_pressDown;
#endif
@end
@@ -0,0 +1,499 @@
//
// ASControlNode.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASControlNode.h>
#import "ASControlNode+Private.h"
#import <AsyncDisplayKit/ASControlNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASControlTargetAction.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASThread.h>
// UIControl allows dragging some distance outside of the control itself during
// tracking. This value depends on the device idiom (25 or 70 points), so
// so replicate that effect with the same values here for our own controls.
#define kASControlNodeExpandedInset (([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? -25.0f : -70.0f)
// Initial capacities for dispatch tables.
#define kASControlNodeEventDispatchTableInitialCapacity 4
#define kASControlNodeActionDispatchTableInitialCapacity 4
@interface ASControlNode ()
{
@private
// Control Attributes
BOOL _enabled;
BOOL _highlighted;
// Tracking
BOOL _tracking;
BOOL _touchInside;
// Target action pairs stored in an array for each event type
// ASControlEvent -> [ASTargetAction0, ASTargetAction1]
NSMutableDictionary<id<NSCopying>, NSMutableArray<ASControlTargetAction *> *> *_controlEventDispatchTable;
}
// Read-write overrides.
@property (getter=isTracking) BOOL tracking;
@property (getter=isTouchInside) BOOL touchInside;
/**
@abstract Returns a key to be used in _controlEventDispatchTable that identifies the control event.
@param controlEvent A control event.
@result A key for use in _controlEventDispatchTable.
*/
id<NSCopying> _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent);
/**
@abstract Enumerates the ASControlNode events included mask, invoking the block for each event.
@param mask An ASControlNodeEvent mask.
@param block The block to be invoked for each ASControlNodeEvent included in mask.
*/
void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent));
/**
@abstract Returns the expanded bounds used to determine if a touch is considered 'inside' during tracking.
@param controlNode A control node.
@result The expanded bounds of the node.
*/
CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode);
@end
@implementation ASControlNode
{
}
#pragma mark - Lifecycle
- (instancetype)init
{
if (!(self = [super init]))
return nil;
_enabled = YES;
// As we have no targets yet, we start off with user interaction off. When a target is added, it'll get turned back on.
self.userInteractionEnabled = NO;
return self;
}
#if TARGET_OS_TV
- (void)didLoad
{
[super didLoad];
// On tvOS all controls, such as buttons, interact with the focus system even if they don't have a target set on them.
// Here we add our own internal tap gesture to handle this behaviour.
self.userInteractionEnabled = YES;
UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_pressDown)];
tapGestureRec.allowedPressTypes = @[@(UIPressTypeSelect)];
[self.view addGestureRecognizer:tapGestureRec];
}
#endif
- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled
{
[super setUserInteractionEnabled:userInteractionEnabled];
self.isAccessibilityElement = userInteractionEnabled;
}
- (void)__exitHierarchy
{
[super __exitHierarchy];
// If a control node is exit the hierarchy and is tracking we have to cancel it
if (self.tracking) {
[self _cancelTrackingWithEvent:nil];
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
#pragma mark - ASDisplayNode Overrides
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// If we're not interested in touches, we have nothing to do.
if (!self.enabled) {
return;
}
// Check if the tracking should start
UITouch *theTouch = [touches anyObject];
if (![self beginTrackingWithTouch:theTouch withEvent:event]) {
return;
}
// If we get more than one touch down on us, cancel.
// Additionally, if we're already tracking a touch, a second touch beginning is cause for cancellation.
if (touches.count > 1 || self.tracking) {
[self _cancelTrackingWithEvent:event];
} else {
// Otherwise, begin tracking.
self.tracking = YES;
// No need to check bounds on touchesBegan as we wouldn't get the call if it wasn't in our bounds.
self.touchInside = YES;
self.highlighted = YES;
// Send the appropriate touch-down control event depending on how many times we've been tapped.
ASControlNodeEvent controlEventMask = (theTouch.tapCount == 1) ? ASControlNodeEventTouchDown : ASControlNodeEventTouchDownRepeat;
[self sendActionsForControlEvents:controlEventMask withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// If we're not interested in touches, we have nothing to do.
if (!self.enabled) {
return;
}
NSParameterAssert(touches.count == 1);
UITouch *theTouch = [touches anyObject];
// Check if tracking should continue
if (!self.tracking || ![self continueTrackingWithTouch:theTouch withEvent:event]) {
self.tracking = NO;
return;
}
CGPoint touchLocation = [theTouch locationInView:self.view];
// Update our touchInside state.
BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil];
// Update our highlighted state.
CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self);
BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
self.touchInside = dragIsInsideExpandedBounds;
self.highlighted = dragIsInsideExpandedBounds;
[self sendActionsForControlEvents:(dragIsInsideBounds ? ASControlNodeEventTouchDragInside : ASControlNodeEventTouchDragOutside)
withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
// If we're not interested in touches, we have nothing to do.
if (!self.enabled) {
return;
}
// Note that we've cancelled tracking.
[self _cancelTrackingWithEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// If we're not interested in touches, we have nothing to do.
if (!self.enabled) {
return;
}
// On iPhone 6s, iOS 9.2 (and maybe other versions) sometimes calls -touchesEnded:withEvent:
// twice on the view for one call to -touchesBegan:withEvent:. On ASControlNode, it used to
// trigger an action twice unintentionally. Now, we ignore that event if we're not in a tracking
// state in order to have a correct behavior.
// It might be related to that issue: http://www.openradar.me/22910171
if (!self.tracking) {
return;
}
NSParameterAssert([touches count] == 1);
UITouch *theTouch = [touches anyObject];
CGPoint touchLocation = [theTouch locationInView:self.view];
// Update state.
self.tracking = NO;
self.touchInside = NO;
self.highlighted = NO;
// Note that we've ended tracking.
[self endTrackingWithTouch:theTouch withEvent:event];
// Send the appropriate touch-up control event.
CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self);
BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
[self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside)
withEvent:event];
}
- (void)_cancelTrackingWithEvent:(UIEvent *)event
{
// We're no longer tracking and there is no touch to be inside.
self.tracking = NO;
self.touchInside = NO;
self.highlighted = NO;
// Send the cancel event.
[self sendActionsForControlEvents:ASControlNodeEventTouchCancel withEvent:event];
}
#pragma clang diagnostic pop
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
ASDisplayNodeAssertMainThread();
// If not enabled we should not care about receving touches
if (! self.enabled) {
return nil;
}
return [super hitTest:point withEvent:event];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
// If we're interested in touches, this is a tap (the only gesture we care about) and passed -hitTest for us, then no, you may not begin. Sir.
if (self.enabled && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && gestureRecognizer.view != self.view) {
UITapGestureRecognizer *tapRecognizer = (UITapGestureRecognizer *)gestureRecognizer;
// Allow double-tap gestures
return tapRecognizer.numberOfTapsRequired != 1;
}
// Otherwise, go ahead. :]
return YES;
}
- (BOOL)supportsLayerBacking
{
return super.supportsLayerBacking && !self.userInteractionEnabled;
}
#pragma mark - Action Messages
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask
{
NSParameterAssert(action);
NSParameterAssert(controlEventMask != 0);
// ASControlNode cannot be layer backed if adding a target
ASDisplayNodeAssert(!self.isLayerBacked, @"ASControlNode is layer backed, will never be able to call target in target:action: pair.");
ASLockScopeSelf();
if (!_controlEventDispatchTable) {
_controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries.
}
// Create new target action pair
ASControlTargetAction *targetAction = [[ASControlTargetAction alloc] init];
targetAction.action = action;
targetAction.target = target;
// Enumerate the events in the mask, adding the target-action pair for each control event included in controlEventMask
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
(ASControlNodeEvent controlEvent)
{
// Do we already have an event table for this control event?
id<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
if (!eventTargetActionArray) {
eventTargetActionArray = [[NSMutableArray alloc] init];
}
// Remove any prior target-action pair for this event, as UIKit does.
[eventTargetActionArray removeObject:targetAction];
// Register the new target-action as the last one to be sent.
[eventTargetActionArray addObject:targetAction];
if (eventKey) {
[_controlEventDispatchTable setObject:eventTargetActionArray forKey:eventKey];
}
});
self.userInteractionEnabled = YES;
}
- (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent
{
NSParameterAssert(target);
NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents);
ASLockScopeSelf();
// Grab the event target action array for this event.
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)];
if (!eventTargetActionArray) {
return nil;
}
NSMutableArray *actions = [[NSMutableArray alloc] init];
// Collect all actions for this target.
for (ASControlTargetAction *targetAction in eventTargetActionArray) {
if ((target == nil && targetAction.createdWithNoTarget) || (target != nil && target == targetAction.target)) {
[actions addObject:NSStringFromSelector(targetAction.action)];
}
}
return actions;
}
- (NSSet *)allTargets
{
ASLockScopeSelf();
NSMutableSet *targets = [[NSMutableSet alloc] init];
// Look at each event...
for (NSMutableArray *eventTargetActionArray in [_controlEventDispatchTable objectEnumerator]) {
// and each event's targets...
for (ASControlTargetAction *targetAction in eventTargetActionArray) {
[targets addObject:targetAction.target];
}
}
return targets;
}
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask
{
NSParameterAssert(controlEventMask != 0);
ASLockScopeSelf();
// Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask.
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
(ASControlNodeEvent controlEvent)
{
// Grab the dispatch table for this event (if we have it).
id<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
if (!eventTargetActionArray) {
return;
}
NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(ASControlTargetAction *_Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
if (!target || evaluatedObject.target == target) {
if (!action) {
return NO;
} else if (evaluatedObject.action == action) {
return NO;
}
}
return YES;
}];
[eventTargetActionArray filterUsingPredicate:filterPredicate];
if (eventTargetActionArray.count == 0) {
// If there are no targets for this event anymore, remove it.
[_controlEventDispatchTable removeObjectForKey:eventKey];
}
});
}
#pragma mark -
- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)event
{
ASDisplayNodeAssertMainThread(); //We access self.view below, it's not safe to call this off of main.
NSParameterAssert(controlEvents != 0);
NSMutableArray *resolvedEventTargetActionArray = [[NSMutableArray<ASControlTargetAction *> alloc] init];
{
ASLockScopeSelf();
// Enumerate the events in the mask, invoking the target-action pairs for each.
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^
(ASControlNodeEvent controlEvent)
{
// Iterate on each target action pair
for (ASControlTargetAction *targetAction in _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]) {
ASControlTargetAction *resolvedTargetAction = [[ASControlTargetAction alloc] init];
resolvedTargetAction.action = targetAction.action;
resolvedTargetAction.target = targetAction.target;
// NSNull means that a nil target was set, so start at self and travel the responder chain
if (!resolvedTargetAction.target && targetAction.createdWithNoTarget) {
// if the target cannot perform the action, travel the responder chain to try to find something that does
resolvedTargetAction.target = [self.view targetForAction:resolvedTargetAction.action withSender:self];
}
if (resolvedTargetAction.target) {
[resolvedEventTargetActionArray addObject:resolvedTargetAction];
}
}
});
}
//We don't want to hold the lock while calling out, we could potentially walk up the ownership tree causing a deadlock.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
for (ASControlTargetAction *targetAction in resolvedEventTargetActionArray) {
[targetAction.target performSelector:targetAction.action withObject:self withObject:event];
}
#pragma clang diagnostic pop
}
#pragma mark - Convenience
id<NSCopying> _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent)
{
return @(controlEvent);
}
void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent))
{
if (block == nil) {
return;
}
// Start with our first event (touch down) and work our way up to the last event (PrimaryActionTriggered)
for (ASControlNodeEvent thisEvent = ASControlNodeEventTouchDown; thisEvent <= ASControlNodeEventPrimaryActionTriggered; thisEvent <<= 1) {
// If it's included in the mask, invoke the block.
if ((mask & thisEvent) == thisEvent)
block(thisEvent);
}
}
CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode) {
return CGRectInset(UIEdgeInsetsInsetRect(controlNode.view.bounds, controlNode.hitTestSlop), kASControlNodeExpandedInset, kASControlNodeExpandedInset);
}
#pragma mark - For Subclasses
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
{
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
{
return YES;
}
- (void)cancelTrackingWithEvent:(UIEvent *)touchEvent
{
// Subclass hook
}
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
{
// Subclass hook
}
#pragma mark - Debug
- (ASDisplayNode *)debugHighlightOverlay
{
return nil;
}
@end
@@ -0,0 +1,65 @@
//
// ASControlTargetAction.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASControlTargetAction.h>
@implementation ASControlTargetAction
{
__weak id _target;
BOOL _createdWithNoTarget;
}
- (void)setTarget:(id)target {
_target = target;
if (!target) {
_createdWithNoTarget = YES;
}
}
- (id)target {
return _target;
}
- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[ASControlTargetAction class]]) {
return NO;
}
ASControlTargetAction *otherObject = (ASControlTargetAction *)object;
BOOL areTargetsEqual;
if (self.target != nil && otherObject.target != nil && self.target == otherObject.target) {
areTargetsEqual = YES;
}
else if (self.target == nil && otherObject.target == nil && self.createdWithNoTarget && otherObject.createdWithNoTarget) {
areTargetsEqual = YES;
}
else {
areTargetsEqual = NO;
}
if (!areTargetsEqual) {
return NO;
}
if (self.action && otherObject.action && self.action == otherObject.action) {
return YES;
}
else {
return NO;
}
}
- (NSUInteger)hash {
return [self.target hash];
}
@end
@@ -0,0 +1,125 @@
//
// ASDimension.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDimension.h>
#import <AsyncDisplayKit/CoreGraphics+ASConvenience.h>
#import <AsyncDisplayKit/ASAssert.h>
#pragma mark - ASDimension
ASDimension const ASDimensionAuto = {ASDimensionUnitAuto, 0};
ASOVERLOADABLE ASDimension ASDimensionMake(NSString *dimension)
{
if (dimension.length > 0) {
// Handle points
if ([dimension hasSuffix:@"pt"]) {
return ASDimensionMake(ASDimensionUnitPoints, ASCGFloatFromString(dimension));
}
// Handle auto
if ([dimension isEqualToString:@"auto"]) {
return ASDimensionAuto;
}
// Handle percent
if ([dimension hasSuffix:@"%"]) {
return ASDimensionMake(ASDimensionUnitFraction, (ASCGFloatFromString(dimension) / 100.0));
}
}
return ASDimensionAuto;
}
NSString *NSStringFromASDimension(ASDimension dimension)
{
switch (dimension.unit) {
case ASDimensionUnitPoints:
return [NSString stringWithFormat:@"%.0fpt", dimension.value];
case ASDimensionUnitFraction:
return [NSString stringWithFormat:@"%.0f%%", dimension.value * 100.0];
case ASDimensionUnitAuto:
return @"Auto";
}
}
#pragma mark - ASLayoutSize
ASLayoutSize const ASLayoutSizeAuto = {ASDimensionAuto, ASDimensionAuto};
#pragma mark - ASSizeRange
ASSizeRange const ASSizeRangeZero = {};
ASSizeRange const ASSizeRangeUnconstrained = { {0, 0}, { INFINITY, INFINITY }};
struct _Range {
CGFloat min;
CGFloat max;
/**
Intersects another dimension range. If the other range does not overlap, this size range "wins" by returning a
single point within its own range that is closest to the non-overlapping range.
*/
_Range intersect(const _Range &other) const
{
CGFloat newMin = MAX(min, other.min);
CGFloat newMax = MIN(max, other.max);
if (newMin <= newMax) {
return {newMin, newMax};
} else {
// No intersection. If we're before the other range, return our max; otherwise our min.
if (min < other.min) {
return {max, max};
} else {
return {min, min};
}
}
}
};
ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRange)
{
const auto w = _Range({sizeRange.min.width, sizeRange.max.width}).intersect({otherSizeRange.min.width, otherSizeRange.max.width});
const auto h = _Range({sizeRange.min.height, sizeRange.max.height}).intersect({otherSizeRange.min.height, otherSizeRange.max.height});
return {{w.min, h.min}, {w.max, h.max}};
}
NSString *NSStringFromASSizeRange(ASSizeRange sizeRange)
{
// 17 field length copied from iOS 10.3 impl of NSStringFromCGSize.
if (CGSizeEqualToSize(sizeRange.min, sizeRange.max)) {
return [NSString stringWithFormat:@"{{%.*g, %.*g}}",
17, sizeRange.min.width,
17, sizeRange.min.height];
}
return [NSString stringWithFormat:@"{{%.*g, %.*g}, {%.*g, %.*g}}",
17, sizeRange.min.width,
17, sizeRange.min.height,
17, sizeRange.max.width,
17, sizeRange.max.height];
}
#if YOGA
#pragma mark - Yoga - ASEdgeInsets
ASEdgeInsets const ASEdgeInsetsZero = {};
ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets)
{
ASEdgeInsets asEdgeInsets = ASEdgeInsetsZero;
asEdgeInsets.top = ASDimensionMake(edgeInsets.top);
asEdgeInsets.left = ASDimensionMake(edgeInsets.left);
asEdgeInsets.bottom = ASDimensionMake(edgeInsets.bottom);
asEdgeInsets.right = ASDimensionMake(edgeInsets.right);
return asEdgeInsets;
}
#endif
@@ -0,0 +1,65 @@
//
// ASDimensionInternal.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDimensionInternal.h>
#pragma mark - ASLayoutElementSize
NSString *NSStringFromASLayoutElementSize(ASLayoutElementSize size)
{
return [NSString stringWithFormat:
@"<ASLayoutElementSize: exact=%@, min=%@, max=%@>",
NSStringFromASLayoutSize(ASLayoutSizeMake(size.width, size.height)),
NSStringFromASLayoutSize(ASLayoutSizeMake(size.minWidth, size.minHeight)),
NSStringFromASLayoutSize(ASLayoutSizeMake(size.maxWidth, size.maxHeight))];
}
ASDISPLAYNODE_INLINE void ASLayoutElementSizeConstrain(CGFloat minVal, CGFloat exactVal, CGFloat maxVal, CGFloat *outMin, CGFloat *outMax)
{
NSCAssert(!isnan(minVal), @"minVal must not be NaN");
NSCAssert(!isnan(maxVal), @"maxVal must not be NaN");
// Avoid use of min/max primitives since they're harder to reason
// about in the presence of NaN (in exactVal)
// Follow CSS: min overrides max overrides exact.
// Begin with the min/max range
*outMin = minVal;
*outMax = maxVal;
if (maxVal <= minVal) {
// min overrides max and exactVal is irrelevant
*outMax = minVal;
return;
}
if (isnan(exactVal)) {
// no exact value, so leave as a min/max range
return;
}
if (exactVal > maxVal) {
// clip to max value
*outMin = maxVal;
} else if (exactVal < minVal) {
// clip to min value
*outMax = minVal;
} else {
// use exact value
*outMin = *outMax = exactVal;
}
}
ASSizeRange ASLayoutElementSizeResolveAutoSize(ASLayoutElementSize size, const CGSize parentSize, ASSizeRange autoASSizeRange)
{
CGSize resolvedExact = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.width, size.height), parentSize, {NAN, NAN});
CGSize resolvedMin = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.minWidth, size.minHeight), parentSize, autoASSizeRange.min);
CGSize resolvedMax = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.maxWidth, size.maxHeight), parentSize, autoASSizeRange.max);
CGSize rangeMin, rangeMax;
ASLayoutElementSizeConstrain(resolvedMin.width, resolvedExact.width, resolvedMax.width, &rangeMin.width, &rangeMax.width);
ASLayoutElementSizeConstrain(resolvedMin.height, resolvedExact.height, resolvedMax.height, &rangeMin.height, &rangeMax.height);
return {rangeMin, rangeMax};
}
@@ -0,0 +1,27 @@
//
// ASDispatch.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
/**
* Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
AS_EXTERN void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i));
/**
* Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
AS_EXTERN void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i));
@@ -0,0 +1,63 @@
//
// ASDispatch.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASDispatch.h"
#import <AsyncDisplayKit/ASConfigurationInternal.h>
// Prefer C atomics in this file because ObjC blocks can't capture C++ atomics well.
#import <stdatomic.h>
/**
* Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) {
if (threadCount == 0) {
if (ASActivateExperimentalFeature(ASExperimentalDispatchApply)) {
dispatch_apply(iterationCount, queue, work);
return;
}
threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2;
}
dispatch_group_t group = dispatch_group_create();
__block atomic_size_t counter = ATOMIC_VAR_INIT(0);
for (NSUInteger t = 0; t < threadCount; t++) {
dispatch_group_async(group, queue, ^{
size_t i;
while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) {
work(i);
}
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
};
/**
* Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) {
if (threadCount == 0) {
threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2;
}
__block atomic_size_t counter = ATOMIC_VAR_INIT(0);
for (NSUInteger t = 0; t < threadCount; t++) {
dispatch_async(queue, ^{
size_t i;
while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) {
work(i);
}
});
}
};
@@ -0,0 +1,90 @@
//
// ASDisplayNode+Ancestry.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDisplayNode+Ancestry.h>
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
AS_SUBCLASSING_RESTRICTED
@interface ASNodeAncestryEnumerator : NSEnumerator
@end
@implementation ASNodeAncestryEnumerator {
ASDisplayNode *_lastNode; // This needs to be strong because enumeration will not retain the current batch of objects
BOOL _initialState;
}
- (instancetype)initWithNode:(ASDisplayNode *)node
{
if (self = [super init]) {
_initialState = YES;
_lastNode = node;
}
return self;
}
- (id)nextObject
{
if (_initialState) {
_initialState = NO;
return _lastNode;
}
ASDisplayNode *nextNode = _lastNode.supernode;
if (nextNode == nil && ASDisplayNodeThreadIsMain()) {
CALayer *layer = _lastNode.nodeLoaded ? _lastNode.layer.superlayer : nil;
while (layer != nil) {
nextNode = ASLayerToDisplayNode(layer);
if (nextNode != nil) {
break;
}
layer = layer.superlayer;
}
}
_lastNode = nextNode;
return nextNode;
}
@end
@implementation ASDisplayNode (Ancestry)
- (id<NSFastEnumeration>)supernodes
{
NSEnumerator *result = [[ASNodeAncestryEnumerator alloc] initWithNode:self];
[result nextObject]; // discard first object (self)
return result;
}
- (id<NSFastEnumeration>)supernodesIncludingSelf
{
return [[ASNodeAncestryEnumerator alloc] initWithNode:self];
}
- (nullable __kindof ASDisplayNode *)supernodeOfClass:(Class)supernodeClass includingSelf:(BOOL)includeSelf
{
id<NSFastEnumeration> chain = includeSelf ? self.supernodesIncludingSelf : self.supernodes;
for (ASDisplayNode *ancestor in chain) {
if ([ancestor isKindOfClass:supernodeClass]) {
return ancestor;
}
}
return nil;
}
- (NSString *)ancestryDescription
{
NSMutableArray *strings = [NSMutableArray array];
for (ASDisplayNode *node in self.supernodes) {
[strings addObject:ASObjectDescriptionMakeTiny(node)];
}
return strings.description;
}
@end
@@ -0,0 +1,443 @@
//
// ASDisplayNode+AsyncDisplay.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/_ASCoreAnimationExtras.h>
#import <AsyncDisplayKit/_ASAsyncTransaction.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASAssert.h>
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASGraphicsContext.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import "ASSignpost.h"
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
using AS::MutexLocker;
@interface ASDisplayNode () <_ASDisplayLayerDelegate>
@end
@implementation ASDisplayNode (AsyncDisplay)
#if ASDISPLAYNODE_DELAY_DISPLAY
#define ASDN_DELAY_FOR_DISPLAY() usleep( (long)(0.1 * USEC_PER_SEC) )
#else
#define ASDN_DELAY_FOR_DISPLAY()
#endif
#define CHECK_CANCELLED_AND_RETURN_NIL(expr) if (isCancelledBlock()) { \
expr; \
return nil; \
} \
- (NSObject *)drawParameters
{
__instanceLock__.lock();
BOOL implementsDrawParameters = _flags.implementsDrawParameters;
__instanceLock__.unlock();
if (implementsDrawParameters) {
return [self drawParametersForAsyncLayer:self.asyncLayer];
} else {
return nil;
}
}
- (void)_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock displayBlocks:(NSMutableArray *)displayBlocks
{
// Skip subtrees that are hidden or zero alpha.
if (self.isHidden || self.alpha <= 0.0) {
return;
}
__instanceLock__.lock();
BOOL rasterizingFromAscendent = (_hierarchyState & ASHierarchyStateRasterized);
__instanceLock__.unlock();
// if super node is rasterizing descendants, subnodes will not have had layout calls because they don't have layers
if (rasterizingFromAscendent) {
[self __layout];
}
// Capture these outside the display block so they are retained.
UIColor *backgroundColor = self.backgroundColor;
CGRect bounds = self.bounds;
CGFloat cornerRadius = self.cornerRadius;
BOOL clipsToBounds = self.clipsToBounds;
CGRect frame;
// If this is the root container node, use a frame with a zero origin to draw into. If not, calculate the correct frame using the node's position, transform and anchorPoint.
if (self.rasterizesSubtree) {
frame = CGRectMake(0.0f, 0.0f, bounds.size.width, bounds.size.height);
} else {
CGPoint position = self.position;
CGPoint anchorPoint = self.anchorPoint;
// Pretty hacky since full 3D transforms aren't actually supported, but attempt to compute the transformed frame of this node so that we can composite it into approximately the right spot.
CGAffineTransform transform = CATransform3DGetAffineTransform(self.transform);
CGSize scaledBoundsSize = CGSizeApplyAffineTransform(bounds.size, transform);
CGPoint origin = CGPointMake(position.x - scaledBoundsSize.width * anchorPoint.x,
position.y - scaledBoundsSize.height * anchorPoint.y);
frame = CGRectMake(origin.x, origin.y, bounds.size.width, bounds.size.height);
}
// Get the display block for this node.
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:NO isCancelledBlock:isCancelledBlock rasterizing:YES];
// We'll display something if there is a display block, clipping, translation and/or a background color.
BOOL shouldDisplay = displayBlock || backgroundColor || CGPointEqualToPoint(CGPointZero, frame.origin) == NO || clipsToBounds;
// If we should display, then push a transform, draw the background color, and draw the contents.
// The transform is popped in a block added after the recursion into subnodes.
if (shouldDisplay) {
dispatch_block_t pushAndDisplayBlock = ^{
// Push transform relative to parent.
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, frame.origin.x, frame.origin.y);
//support cornerRadius
if (rasterizingFromAscendent && clipsToBounds) {
if (cornerRadius) {
[[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius] addClip];
} else {
CGContextClipToRect(context, bounds);
}
}
// Fill background if any.
CGColorRef backgroundCGColor = backgroundColor.CGColor;
if (backgroundColor && CGColorGetAlpha(backgroundCGColor) > 0.0) {
CGContextSetFillColorWithColor(context, backgroundCGColor);
CGContextFillRect(context, bounds);
}
// If there is a display block, call it to get the image, then copy the image into the current context (which is the rasterized container's backing store).
if (displayBlock) {
UIImage *image = (UIImage *)displayBlock();
if (image) {
BOOL opaque = ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage));
CGBlendMode blendMode = opaque ? kCGBlendModeCopy : kCGBlendModeNormal;
[image drawInRect:bounds blendMode:blendMode alpha:1];
}
}
};
[displayBlocks addObject:pushAndDisplayBlock];
}
// Recursively capture displayBlocks for all descendants.
for (ASDisplayNode *subnode in self.subnodes) {
[subnode _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];
}
// If we pushed a transform, pop it by adding a display block that does nothing other than that.
if (shouldDisplay) {
// Since this block is pure, we can store it statically.
static dispatch_block_t popBlock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
popBlock = ^{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextRestoreGState(context);
};
});
[displayBlocks addObject:popBlock];
}
}
- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous
isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock
rasterizing:(BOOL)rasterizing
{
ASDisplayNodeAssertMainThread();
asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
ASDisplayNodeFlags flags;
__instanceLock__.lock();
flags = _flags;
// We always create a graphics context, unless a -display method is used, OR if we are a subnode drawing into a rasterized parent.
BOOL shouldCreateGraphicsContext = (flags.implementsImageDisplay == NO && rasterizing == NO);
BOOL shouldBeginRasterizing = (rasterizing == NO && flags.rasterizesSubtree);
BOOL usesImageDisplay = flags.implementsImageDisplay;
BOOL usesDrawRect = flags.implementsDrawRect;
if (usesImageDisplay == NO && usesDrawRect == NO && shouldBeginRasterizing == NO) {
// Early exit before requesting more expensive properties like bounds and opaque from the layer.
__instanceLock__.unlock();
return nil;
}
BOOL opaque = self.opaque;
CGRect bounds = self.bounds;
UIColor *backgroundColor = self.backgroundColor;
CGColorRef borderColor = self.borderColor;
CGFloat borderWidth = self.borderWidth;
CGFloat contentsScaleForDisplay = _contentsScaleForDisplay;
__instanceLock__.unlock();
// Capture drawParameters from delegate on main thread, if this node is displaying itself rather than recursively rasterizing.
id drawParameters = (shouldBeginRasterizing == NO ? [self drawParameters] : nil);
// Only the -display methods should be called if we can't size the graphics buffer to use.
if (CGRectIsEmpty(bounds) && (shouldBeginRasterizing || shouldCreateGraphicsContext)) {
return nil;
}
ASDisplayNodeAssert(contentsScaleForDisplay != 0.0, @"Invalid contents scale");
ASDisplayNodeAssert(rasterizing || !(_hierarchyState & ASHierarchyStateRasterized),
@"Rasterized descendants should never display unless being drawn into the rasterized container.");
if (shouldBeginRasterizing) {
// Collect displayBlocks for all descendants.
NSMutableArray *displayBlocks = [[NSMutableArray alloc] init];
[self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];
CHECK_CANCELLED_AND_RETURN_NIL();
// If [UIColor clearColor] or another semitransparent background color is used, include alpha channel when rasterizing.
// Unlike CALayer drawing, we include the backgroundColor as a base during rasterization.
opaque = opaque && CGColorGetAlpha(backgroundColor.CGColor) == 1.0f;
displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
UIImage *image = ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, ^{
for (dispatch_block_t block in displayBlocks) {
if (isCancelledBlock()) return;
block();
}
});
ASDN_DELAY_FOR_DISPLAY();
return image;
};
} else {
displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
__block UIImage *image = nil;
void (^workWithContext)() = ^{
CGContextRef currentContext = UIGraphicsGetCurrentContext();
if (shouldCreateGraphicsContext && !currentContext) {
ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
return;
}
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];
if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
}
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
ASDN_DELAY_FOR_DISPLAY();
};
if (shouldCreateGraphicsContext) {
return ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, workWithContext);
} else {
workWithContext();
return image;
}
};
}
/**
If we're profiling, wrap the display block with signpost start and end.
Color the interval red if cancelled, green otherwise.
*/
#if AS_KDEBUG_ENABLE
__unsafe_unretained id ptrSelf = self;
displayBlock = ^{
ASSignpostStartCustom(ASSignpostLayerDisplay, ptrSelf, 0);
id result = displayBlock();
ASSignpostEndCustom(ASSignpostLayerDisplay, ptrSelf, 0, isCancelledBlock() ? ASSignpostColorRed : ASSignpostColorGreen);
return result;
};
#endif
return displayBlock;
}
- (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawParameters:(id _Nullable)drawParameters
{
if (context) {
__instanceLock__.lock();
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
CGFloat cornerRadius = _cornerRadius;
ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext;
__instanceLock__.unlock();
if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0) {
ASDisplayNodeAssert(context == UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self);
// TODO: This clip path should be removed if we are rasterizing.
CGRect boundingBox = CGContextGetClipBoundingBox(context);
[[UIBezierPath bezierPathWithRoundedRect:boundingBox cornerRadius:cornerRadius] addClip];
}
if (willDisplayNodeContentWithRenderingContext) {
willDisplayNodeContentWithRenderingContext(context, drawParameters);
}
}
}
- (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:(UIImage **)image drawParameters:(id _Nullable)drawParameters backgroundColor:(UIColor *)backgroundColor borderWidth:(CGFloat)borderWidth borderColor:(CGColorRef)borderColor
{
if (context == NULL && *image == NULL) {
return;
}
__instanceLock__.lock();
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
__instanceLock__.unlock();
if (context != NULL) {
if (didDisplayNodeContentWithRenderingContext) {
didDisplayNodeContentWithRenderingContext(context, drawParameters);
}
}
}
- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously
{
ASDisplayNodeAssertMainThread();
__instanceLock__.lock();
if (_hierarchyState & ASHierarchyStateRasterized) {
__instanceLock__.unlock();
return;
}
CALayer *layer = _layer;
BOOL rasterizesSubtree = _flags.rasterizesSubtree;
__instanceLock__.unlock();
// for async display, capture the current displaySentinel value to bail early when the job is executed if another is
// enqueued
// for sync display, do not support cancellation
// FIXME: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing
// from the displayQueue? Need to not cancel early fails from displaySentinel changes.
asdisplaynode_iscancelled_block_t isCancelledBlock = nil;
if (asynchronously) {
uint displaySentinelValue = ++_displaySentinel;
__weak ASDisplayNode *weakSelf = self;
isCancelledBlock = ^BOOL{
__strong ASDisplayNode *self = weakSelf;
return self == nil || (displaySentinelValue != self->_displaySentinel.load());
};
} else {
isCancelledBlock = ^BOOL{
return NO;
};
}
// Set up displayBlock to call either display or draw on the delegate and return a UIImage contents
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];
if (!displayBlock) {
return;
}
ASDisplayNodeAssert(layer, @"Expect _layer to be not nil");
// This block is called back on the main thread after rendering at the completion of the current async transaction, or immediately if !asynchronously
asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
ASDisplayNodeCAssertMainThread();
if (!canceled && !isCancelledBlock()) {
UIImage *image = (UIImage *)value;
BOOL stretchable = (NO == UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero));
if (stretchable) {
ASDisplayNodeSetResizableContents(layer, image);
} else {
layer.contentsScale = self.contentsScale;
layer.contents = (id)image.CGImage;
}
[self didDisplayAsyncLayer:self.asyncLayer];
if (rasterizesSubtree) {
ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) {
[node didDisplayAsyncLayer:node.asyncLayer];
});
}
}
};
// Call willDisplay immediately in either case
[self willDisplayAsyncLayer:self.asyncLayer asynchronously:asynchronously];
if (rasterizesSubtree) {
ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) {
[node willDisplayAsyncLayer:node.asyncLayer asynchronously:asynchronously];
});
}
if (asynchronously) {
// Async rendering operations are contained by a transaction, which allows them to proceed and concurrently
// while synchronizing the final application of the results to the layer's contents property (completionBlock).
// First, look to see if we are expected to join a parent's transaction container.
CALayer *containerLayer = layer.asyncdisplaykit_parentTransactionContainer ? : layer;
// In the case that a transaction does not yet exist (such as for an individual node outside of a container),
// this call will allocate the transaction and add it to _ASAsyncTransactionGroup.
// It will automatically commit the transaction at the end of the runloop.
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
// Adding this displayBlock operation to the transaction will start it IMMEDIATELY.
// The only function of the transaction commit is to gate the calling of the completionBlock.
[transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
} else {
UIImage *contents = (UIImage *)displayBlock();
completionBlock(contents, NO);
}
}
- (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer
{
_displaySentinel.fetch_add(1);
}
- (ASDisplayNodeContextModifier)willDisplayNodeContentWithRenderingContext
{
MutexLocker l(__instanceLock__);
return _willDisplayNodeContentWithRenderingContext;
}
- (ASDisplayNodeContextModifier)didDisplayNodeContentWithRenderingContext
{
MutexLocker l(__instanceLock__);
return _didDisplayNodeContentWithRenderingContext;
}
- (void)setWillDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier
{
MutexLocker l(__instanceLock__);
_willDisplayNodeContentWithRenderingContext = contextModifier;
}
- (void)setDidDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier;
{
MutexLocker l(__instanceLock__);
_didDisplayNodeContentWithRenderingContext = contextModifier;
}
@end
@@ -0,0 +1,40 @@
//
// ASDisplayNode+Convenience.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDisplayNode+Convenience.h>
#import <UIKit/UIViewController.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import "ASResponderChainEnumerator.h"
@implementation ASDisplayNode (Convenience)
- (__kindof UIViewController *)closestViewController
{
ASDisplayNodeAssertMainThread();
// Careful not to trigger node loading here.
if (!self.nodeLoaded) {
return nil;
}
// Get the closest view.
UIView *view = ASFindClosestViewOfLayer(self.layer);
// Travel up the responder chain to find a view controller.
for (UIResponder *responder in [view asdk_responderChainEnumerator]) {
UIViewController *vc = ASDynamicCast(responder, UIViewController);
if (vc != nil) {
return vc;
}
}
return nil;
}
@end
@@ -0,0 +1,142 @@
//
// ASDisplayNode+Deprecated.h
// Texture
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#pragma once
#import <AsyncDisplayKit/ASDisplayNode.h>
@interface ASDisplayNode (Deprecated)
/**
* @abstract The name of this node, which will be displayed in `description`. The default value is nil.
*
* @deprecated Deprecated in version 2.0: Use .debugName instead. This value will display in
* results of the -asciiArtString method (@see ASLayoutElementAsciiArtProtocol).
*/
@property (nullable, nonatomic, copy) NSString *name ASDISPLAYNODE_DEPRECATED_MSG("Use .debugName instead.");
/**
* @abstract Provides a default intrinsic content size for calculateSizeThatFits:. This is useful when laying out
* a node that either has no intrinsic content size or should be laid out at a different size than its intrinsic content
* size. For example, this property could be set on an ASImageNode to display at a size different from the underlying
* image size.
*
* @return Try to create a CGSize for preferredFrameSize of this node from the width and height property of this node. It will return CGSizeZero if width and height dimensions are not of type ASDimensionUnitPoints.
*
* @deprecated Deprecated in version 2.0: Just calls through to set the height and width property of the node. Convert to use sizing properties instead: height, minHeight, maxHeight, width, minWidth, maxWidth.
*/
@property (nonatomic, assign, readwrite) CGSize preferredFrameSize ASDISPLAYNODE_DEPRECATED_MSG("Use .style.preferredSize instead OR set individual values with .style.height and .style.width.");
/**
* @abstract Asks the node to measure and return the size that best fits its subnodes.
*
* @param constrainedSize The maximum size the receiver should fit in.
*
* @return A new size that fits the receiver's subviews.
*
* @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the
* constraint and the result.
*
* @warning Subclasses must not override this; it calls -measureWithSizeRange: with zero min size.
* -measureWithSizeRange: caches results from -calculateLayoutThatFits:. Calling this method may
* be expensive if result is not cached.
*
* @see measureWithSizeRange:
* @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:]
*
* @deprecated Deprecated in version 2.0: Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout
*/
- (CGSize)measure:(CGSize)constrainedSize/* ASDISPLAYNODE_DEPRECATED_MSG("Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout.")*/;
ASLayoutElementStyleForwardingDeclaration
/**
* @abstract Called whenever the visiblity of the node changed.
*
* @discussion Subclasses may use this to monitor when they become visible.
*
* @deprecated @see didEnterVisibleState @see didExitVisibleState
*/
- (void)visibilityDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterVisibleState / -didExitVisibleState instead.");
/**
* @abstract Called whenever the visiblity of the node changed.
*
* @discussion Subclasses may use this to monitor when they become visible.
*
* @deprecated @see didEnterVisibleState @see didExitVisibleState
*/
- (void)visibleStateDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterVisibleState / -didExitVisibleState instead.");
/**
* @abstract Called whenever the the node has entered or exited the display state.
*
* @discussion Subclasses may use this to monitor when a node should be rendering its content.
*
* @note This method can be called from any thread and should therefore be thread safe.
*
* @deprecated @see didEnterDisplayState @see didExitDisplayState
*/
- (void)displayStateDidChange:(BOOL)inDisplayState ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterDisplayState / -didExitDisplayState instead.");
/**
* @abstract Called whenever the the node has entered or left the load state.
*
* @discussion Subclasses may use this to monitor data for a node should be loaded, either from a local or remote source.
*
* @note This method can be called from any thread and should therefore be thread safe.
*
* @deprecated @see didEnterPreloadState @see didExitPreloadState
*/
- (void)loadStateDidChange:(BOOL)inLoadState ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState / -didExitPreloadState instead.");
/**
* @abstract Cancels all performing layout transitions. Can be called on any thread.
*
* @deprecated Deprecated in version 2.0: Use cancelLayoutTransition
*/
- (void)cancelLayoutTransitionsInProgress ASDISPLAYNODE_DEPRECATED_MSG("Use -cancelLayoutTransition instead.");
/**
* @abstract A boolean that shows whether the node automatically inserts and removes nodes based on the presence or
* absence of the node and its subnodes is completely determined in its layoutSpecThatFits: method.
*
* @discussion If flag is YES the node no longer require addSubnode: or removeFromSupernode method calls. The presence
* or absence of subnodes is completely determined in its layoutSpecThatFits: method.
*
* @deprecated Deprecated in version 2.0: Use automaticallyManagesSubnodes
*/
@property (nonatomic, assign) BOOL usesImplicitHierarchyManagement ASDISPLAYNODE_DEPRECATED_MSG("Set .automaticallyManagesSubnodes instead.");
/**
* @abstract Indicates that the node should fetch any external data, such as images.
*
* @discussion Subclasses may override this method to be notified when they should begin to preload. Fetching
* should be done asynchronously. The node is also responsible for managing the memory of any data.
* The data may be remote and accessed via the network, but could also be a local database query.
*/
- (void)fetchData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState instead.");
/**
* Provides an opportunity to clear any fetched data (e.g. remote / network or database-queried) on the current node.
*
* @discussion This will not clear data recursively for all subnodes. Either call -recursivelyClearPreloadedData or
* selectively clear fetched data.
*/
- (void)clearFetchedData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didExitPreloadState instead.");
@end

Some files were not shown because too many files have changed in this diff Show More