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