Merge commit 'f174769d2d' into beta

This commit is contained in:
Ilya Laktyushin 2024-10-05 19:47:43 +04:00
commit 525f8cc179
90 changed files with 4489 additions and 1385 deletions

4
.gitignore vendored
View file

@ -65,3 +65,7 @@ xcodeproj.bazelrc
build-input/*
**/*.pyc
*.pyc
submodules/OpusBinding/SharedHeaders/*
submodules/FFMpegBinding/SharedHeaders/*
submodules/OpenSSLEncryptionProvider/SharedHeaders/*

View file

@ -12950,7 +12950,7 @@ Sorry for the inconvenience.";
"SharedMedia.GiftCount_any" = "%@ gifts";
"Stars.Info.Title" = "What are Stars?";
"Stars.Info.Description" = "Buy packages of Stars on Telegram that let you do following:";
"Stars.Info.Description" = "Telegram Stars allow users to:";
"Stars.Info.Gift.Title" = "Send Gifts to Friends";
"Stars.Info.Gift.Text" = "Give your friends gifts that can be kept on their profiles or converted to Stars.";
"Stars.Info.Miniapp.Title" = "Use Stars in Miniapps";
@ -12976,6 +12976,7 @@ Sorry for the inconvenience.";
"Gift.View.HiddenName" = "Hidden Name";
"Gift.View.Date" = "Date";
"Gift.View.Availability" = "Availability";
"Gift.View.Availability.Of" = "%1$@ of %2$@";
"Gift.View.Hide" = "Hide from My Page";
"Gift.View.Display" = "Display on My Page";
"Gift.View.Convert" = "Convert to %@";
@ -12986,7 +12987,7 @@ Sorry for the inconvenience.";
"Gift.Hidden.Title" = "Gift Removed from Profile";
"Gift.Hidden.Text" = "The gift is no longer displayed in [your profile]().";
"Gift.Convert.Title" = "Convert Gift to Stars";
"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis action cannot be undone.";
"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis will permanently destroy the gift.";
"Gift.Convert.Stars_1" = "%@ Star";
"Gift.Convert.Stars_any" = "%@ Stars";
"Gift.Convert.Convert" = "Convert";
@ -13032,10 +13033,12 @@ Sorry for the inconvenience.";
"Gift.Send.Title" = "Send a Gift";
"Gift.Send.Customize.Title" = "CUSTOMIZE YOUR GIFT";
"Gift.Send.Customize.MessagePlaceholder" = "Enter Message";
"Gift.Send.Customize.MessagePlaceholder" = "Enter Message (Optional)";
"Gift.Send.Customize.Info" = "Only %@ will see your message.";
"Gift.Send.HideMyName" = "Hide My Name";
"Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message.";
"Gift.Send.Send" = "Send a Gift for";
"Gift.Send.Limited" = "Limited";
"Profile.SendGift" = "Send a Gift";
"Settings.SendGift" = "Send a Gift";
@ -13047,7 +13050,22 @@ Sorry for the inconvenience.";
"Report.Title.User" = "Report User";
"Report.Title.Group" = "Report Group";
"Report.Title.Channel" = "Report Channel";
"Report.Title.Bot" = "Report Bot";
"Report.Comment.Placeholder" = "Add Comment";
"Report.Comment.Placeholder.Optional" = "Add Comment (Optional)";
"Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected.";
"Report.Send" = "Send Report";
"Notification.PremiumGift.MonthsTitle_1" = "%@ Month Premium";
"Notification.PremiumGift.MonthsTitle_any" = "%@ Months Premium";
"Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium";
"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium";
"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features.";
"Notification.StarsGift.Stars_1" = "%@ Star";
"Notification.StarsGift.Stars_any" = "%@ Stars";
"WebBrowser.AuthChallenge.Title" = "Sign in to %@";
"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely.";

View file

@ -1041,7 +1041,7 @@ public protocol SharedAccountContext: AnyObject {
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool)
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?)
func makeDebugSettingsController(context: AccountContext?) -> ViewController?

View file

@ -202,6 +202,7 @@ public protocol UniversalVideoManager: AnyObject {
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 {

View file

@ -143,6 +143,7 @@ public struct PremiumConfiguration {
showPremiumGiftInTextField: false,
giveawayGiftsPurchaseAvailable: false,
starsGiftsPurchaseAvailable: false,
starGiftsPurchaseBlocked: true,
boostsPerGiftCount: 3,
audioTransciptionTrialMaxDuration: 300,
audioTransciptionTrialCount: 2,
@ -170,6 +171,7 @@ public struct PremiumConfiguration {
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
@ -196,6 +198,7 @@ public struct PremiumConfiguration {
showPremiumGiftInTextField: Bool,
giveawayGiftsPurchaseAvailable: Bool,
starsGiftsPurchaseAvailable: Bool,
starGiftsPurchaseBlocked: Bool,
boostsPerGiftCount: Int32,
audioTransciptionTrialMaxDuration: Int32,
audioTransciptionTrialCount: Int32,
@ -221,6 +224,7 @@ public struct PremiumConfiguration {
self.showPremiumGiftInTextField = showPremiumGiftInTextField
self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable
self.starsGiftsPurchaseAvailable = starsGiftsPurchaseAvailable
self.starGiftsPurchaseBlocked = starGiftsPurchaseBlocked
self.boostsPerGiftCount = boostsPerGiftCount
self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration
self.audioTransciptionTrialCount = audioTransciptionTrialCount
@ -254,6 +258,7 @@ public struct PremiumConfiguration {
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,

View file

@ -19,8 +19,9 @@ 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, transition: ContainedViewLayoutTransition)
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition)
func play()
func pause()
@ -41,6 +42,8 @@ public protocol UniversalVideoContentNode: AnyObject {
func fetchControl(_ control: UniversalVideoNodeFetchControl)
func notifyPlaybackControlsHidden(_ hidden: Bool)
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool)
func enterNativePictureInPicture() -> Bool
func exitNativePictureInPicture()
}
public protocol UniversalVideoContent {
@ -68,7 +71,7 @@ public protocol UniversalVideoDecoration: AnyObject {
func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?)
func updateContentNodeSnapshot(_ snapshot: UIView?)
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition)
func tap()
}
@ -100,7 +103,7 @@ public final class UniversalVideoNode: ASDisplayNode {
private let autoplay: Bool
private let snapshotContentWhenGone: Bool
private var contentNode: (UniversalVideoContentNode & ASDisplayNode)?
private(set) var contentNode: (UniversalVideoContentNode & ASDisplayNode)?
private var contentNodeId: Int32?
private var playbackCompletedIndex: Int?
@ -125,6 +128,11 @@ public final class UniversalVideoNode: ASDisplayNode {
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()
@ -181,6 +189,7 @@ public final class UniversalVideoNode: ASDisplayNode {
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)
@ -247,8 +256,8 @@ public final class UniversalVideoNode: ASDisplayNode {
}
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.decoration.updateLayout(size: size, transition: transition)
public func updateLayout(size: CGSize, actualSize: CGSize? = nil, transition: ContainedViewLayoutTransition) {
self.decoration.updateLayout(size: size, actualSize: actualSize ?? size, transition: transition)
}
public func play() {
@ -418,4 +427,22 @@ public final class UniversalVideoNode: ASDisplayNode {
}
})
}
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()
}
})
}
}

View file

@ -67,7 +67,7 @@ private class AvatarNodeParameters: NSObject {
}
}
private func calculateColors(context: AccountContext?, explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] {
public func calculateAvatarColors(context: AccountContext?, explicitColorIndex: Int?, peerId: EnginePeer.Id?, nameColor: PeerNameColor?, icon: AvatarNodeIcon, theme: PresentationTheme?) -> [UIColor] {
let colorIndex: Int
if let explicitColorIndex = explicitColorIndex {
colorIndex = explicitColorIndex
@ -183,7 +183,7 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
}
}
private enum AvatarNodeIcon: Equatable {
public enum AvatarNodeIcon: Equatable {
case none
case savedMessagesIcon
case repliesIcon
@ -577,7 +577,7 @@ public final class AvatarNode: ASDisplayNode {
self.editOverlayNode?.isHidden = true
}
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer.id, colors: calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
} else {
self.imageReady.set(.single(true))
self.displaySuspended = false
@ -586,7 +586,7 @@ public final class AvatarNode: ASDisplayNode {
}
self.editOverlayNode?.isHidden = true
let colors = calculateColors(context: nil, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
let colors = calculateAvatarColors(context: nil, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: accountPeerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
if let badgeView = self.badgeView {
@ -754,7 +754,7 @@ public final class AvatarNode: ASDisplayNode {
self.editOverlayNode?.isHidden = true
}
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer.id, colors: calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer.id, nameColor: peer.nameColor, icon: icon, theme: theme), letters: peer.displayLetters, font: self.font, icon: icon, explicitColorIndex: nil, hasImage: true, clipStyle: clipStyle)
} else {
self.imageReady.set(.single(true))
self.displaySuspended = false
@ -763,7 +763,7 @@ public final class AvatarNode: ASDisplayNode {
}
self.editOverlayNode?.isHidden = true
let colors = calculateColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
let colors = calculateAvatarColors(context: genericContext, explicitColorIndex: nil, peerId: peer?.id ?? EnginePeer.Id(0), nameColor: peer?.nameColor, icon: icon, theme: theme)
parameters = AvatarNodeParameters(theme: theme, accountPeerId: account.peerId, peerId: peer?.id ?? EnginePeer.Id(0), colors: colors, letters: peer?.displayLetters ?? [], font: self.font, icon: icon, explicitColorIndex: nil, hasImage: false, clipStyle: clipStyle)
if let badgeView = self.badgeView {
@ -800,9 +800,9 @@ public final class AvatarNode: ASDisplayNode {
let parameters: AvatarNodeParameters
if let icon = icon, case .phone = icon {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .phoneIcon, theme: nil), letters: [], font: self.font, icon: .phoneIcon, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
} else {
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
parameters = AvatarNodeParameters(theme: nil, accountPeerId: nil, peerId: nil, colors: calculateAvatarColors(context: nil, explicitColorIndex: explicitIndex, peerId: nil, nameColor: nil, icon: .none, theme: nil), letters: letters, font: self.font, icon: .none, explicitColorIndex: explicitIndex, hasImage: false, clipStyle: .round)
}
self.displaySuspended = true

View file

@ -325,7 +325,7 @@ private final class VideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init() {
self.contentContainerNode = ASDisplayNode()
@ -345,9 +345,9 @@ private final class VideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -405,8 +405,8 @@ private final class VideoDecoration: UniversalVideoDecoration {
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
@ -421,7 +421,7 @@ private final class VideoDecoration: UniversalVideoDecoration {
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}

View file

@ -102,7 +102,7 @@ final class CameraDeviceContext {
return 30.0
}
switch DeviceModel.current {
case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax:
case .iPhone15ProMax, .iPhone14ProMax, .iPhone13ProMax, .iPhone16ProMax:
return 60.0
default:
return 30.0

View file

@ -34,6 +34,10 @@ public extension Camera {
self = .iPhone15Pro
case .iPhone15ProMax:
self = .iPhone15ProMax
case .iPhone16Pro:
self = .iPhone15Pro
case .iPhone16ProMax:
self = .iPhone15ProMax
case .unknown:
self = .unknown
default:

View file

@ -399,7 +399,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType)
case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool)
case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool)
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool)
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?)
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool)
case addContact(String, PresentationTheme, PresentationStrings)
@ -411,7 +411,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return .localPeerId(peer.id)
case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _):
return .localPeerId(peer.id)
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _):
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _):
return .globalPeerId(peer.peer.id)
case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _):
return .messageId(message.id, section)
@ -440,8 +440,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else {
return false
}
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging):
if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging {
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging, lhsQuery):
if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging, rhsQuery) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging, lhsQuery == rhsQuery {
return true
} else {
return false
@ -543,11 +543,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case .globalPeer, .message, .addContact:
return true
}
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _):
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _):
switch rhs {
case .topic, .recentlySearchedPeer, .localPeer:
return false
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _):
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex
case .message, .addContact:
return true
@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openStories(peer.id, sourceNode.avatarNode)
}
})
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging):
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query):
var enabled = true
if filter.contains(.onlyWriteable) {
enabled = canSendMessagesToPeer(peer.peer)
@ -822,7 +822,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
var suffixString = ""
if let subscribers = peer.subscribers, subscribers != 0 {
if peer.peer is TelegramUser {
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))"
} else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
} else {
@ -858,7 +858,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
isSavedMessages = true
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, action: { _ in
interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil)
}, disabledAction: { _ in
interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic)
@ -2607,7 +2607,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
existingPeerIds.insert(peer.peer.id)
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false))
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery))
index += 1
numberOfGlobalPeers += 1
}
@ -2953,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if case let .user(user) = peer, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(peer.id)
}
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _):
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _):
storyStatsIds.append(foundPeer.peer.id)
if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id)
@ -2994,8 +2994,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false)
case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _):
mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false)
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _):
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false)
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _, searchQuery):
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false, searchQuery)
case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _, _):
mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false)
default:
@ -3672,7 +3672,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
url: "",
simple: true,
source: .generic,
skipTermsOfService: true
skipTermsOfService: true,
payload: nil
)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(

View file

@ -1714,9 +1714,20 @@ public final class ChatListNode: ListView {
guard let self else {
return
}
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil)
controller.navigationPresentation = .modal
self.push?(controller)
if let birthdays, birthdays.count == 1, let peerId = birthdays.keys.first {
let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
|> filter { !$0.isEmpty }
|> deliverOnMainQueue).start(next: { giftOptions in
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: premiumOptions)
controller.navigationPresentation = .modal
self.push?(controller)
})
} else {
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil)
controller.navigationPresentation = .modal
self.push?(controller)
}
}, openPremiumManagement: { [weak self] in
guard let self else {
return

View file

@ -8,11 +8,15 @@ public final class BundleIconComponent: Component {
public let name: String
public let tintColor: UIColor?
public let maxSize: CGSize?
public let shadowColor: UIColor?
public let shadowBlur: CGFloat
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil) {
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0) {
self.name = name
self.tintColor = tintColor
self.maxSize = maxSize
self.shadowColor = shadowColor
self.shadowBlur = shadowBlur
}
public static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool {
@ -25,6 +29,12 @@ public final class BundleIconComponent: Component {
if lhs.maxSize != rhs.maxSize {
return false
}
if lhs.shadowColor != rhs.shadowColor {
return false
}
if lhs.shadowBlur != rhs.shadowBlur {
return false
}
return true
}
@ -40,12 +50,24 @@ public final class BundleIconComponent: Component {
}
func update(component: BundleIconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if self.component?.name != component.name || self.component?.tintColor != component.tintColor {
if self.component?.name != component.name || self.component?.tintColor != component.tintColor || self.component?.shadowColor != component.shadowColor || self.component?.shadowBlur != component.shadowBlur {
var image: UIImage?
if let tintColor = component.tintColor {
self.image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil)
image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil)
} else {
self.image = UIImage(bundleImageName: component.name)
image = UIImage(bundleImageName: component.name)
}
if let imageValue = image, let shadowColor = component.shadowColor, component.shadowBlur != 0.0 {
image = generateImage(CGSize(width: imageValue.size.width + component.shadowBlur * 2.0, height: imageValue.size.height + component.shadowBlur * 2.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: component.shadowBlur, color: shadowColor.cgColor)
if let cgImage = imageValue.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: component.shadowBlur, y: component.shadowBlur), size: imageValue.size))
}
})
}
self.image = image
}
self.component = component

View file

@ -175,6 +175,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let options: [ItemListPeerItemRevealOption]
let additionalActions: [ContactsPeerItemAction]
let actionIcon: ContactsPeerItemActionIcon
let searchQuery: String?
let action: ((ContactsPeerItemPeer) -> Void)?
let disabledAction: ((ContactsPeerItemPeer) -> Void)?
let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)?
@ -215,6 +216,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
actionIcon: ContactsPeerItemActionIcon = .none,
index: SortIndex?,
header: ListViewItemHeader?,
searchQuery: String? = nil,
action: ((ContactsPeerItemPeer) -> Void)?,
disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil,
setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil,
@ -245,6 +247,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.options = options
self.additionalActions = additionalActions
self.actionIcon = actionIcon
self.searchQuery = searchQuery
self.action = action
self.disabledAction = disabledAction
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
@ -880,7 +883,16 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
}
case let .addressName(suffix):
if let addressName = peer.addressName {
var addressName = peer.addressName
if let currentAddressName = addressName, let searchQuery = item.searchQuery?.lowercased(), !peer.usernames.isEmpty && !currentAddressName.lowercased().contains(searchQuery) {
for username in peer.usernames {
if username.username.lowercased().contains(searchQuery) {
addressName = username.username
break
}
}
}
if let addressName {
let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor)
if !suffix.isEmpty {
let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)

View file

@ -36,6 +36,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
case iPhone14ProZoomed
case iPhone14ProMax
case iPhone14ProMaxZoomed
case iPhone16Pro
case iPhone16ProMax
case iPad
case iPadMini
case iPad102Inch
@ -68,6 +70,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
.iPhone14ProZoomed,
.iPhone14ProMax,
.iPhone14ProMaxZoomed,
.iPhone16Pro,
.iPhone16ProMax,
.iPad,
.iPadMini,
.iPad102Inch,
@ -171,6 +175,10 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return CGSize(width: 430.0, height: 932.0)
case .iPhone14ProMaxZoomed:
return CGSize(width: 375.0, height: 812.0)
case .iPhone16Pro:
return CGSize(width: 402.0, height: 874.0)
case .iPhone16ProMax:
return CGSize(width: 440.0, height: 956.0)
case .iPad:
return CGSize(width: 768.0, height: 1024.0)
case .iPadMini:
@ -204,6 +212,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 53.0 + UIScreenPixel
case .iPhone14Pro, .iPhone14ProMax:
return 55.0
case .iPhone16Pro, .iPhone16ProMax:
return 55.0
case let .unknown(_, _, _, screenCornerRadius):
return screenCornerRadius
default:
@ -213,7 +223,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func safeInsets(inLandscape: Bool) -> UIEdgeInsets {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
default:
return UIEdgeInsets.zero
@ -222,7 +232,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? {
switch self {
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax:
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProMax, .iPhone16Pro, .iPhone16ProMax:
return inLandscape ? 21.0 : 34.0
case .iPhone14ProZoomed:
return inLandscape ? 21.0 : 28.0
@ -262,6 +272,8 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 54.0
case .iPhone14ProMaxZoomed:
return 47.0
case .iPhone16Pro, .iPhone16ProMax:
return 54.0
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax:
return 44.0
case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
@ -280,7 +292,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 162.0
case .iPhone6, .iPhone6Plus:
return 163.0
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
return 172.0
case .iPad, .iPad102Inch, .iPadPro10Inch:
return 348.0
@ -299,9 +311,9 @@ public enum DeviceMetrics: CaseIterable, Equatable {
return 216.0
case .iPhone6Plus:
return 226.0
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed:
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro:
return 292.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
return 302.0
case .iPad, .iPad102Inch, .iPadPro10Inch:
return 263.0
@ -320,7 +332,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
if inLandscape {
switch self {
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
return 37.0
case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen:
return 50.0
@ -331,7 +343,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
switch self {
case .iPhone4, .iPhone5:
return 37.0
case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
return 44.0
case .iPhone6Plus:
return 45.0
@ -358,7 +370,7 @@ public enum DeviceMetrics: CaseIterable, Equatable {
public var hasDynamicIsland: Bool {
switch self {
case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed:
case .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMax, .iPhone14ProMaxZoomed, .iPhone16Pro, .iPhone16ProMax:
return true
default:
return false

View file

@ -1,20 +0,0 @@
import Foundation
import UIKit
final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate {
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.cancelsTouchesInView = false
self.delaysTouchesBegan = false
self.delaysTouchesEnded = false
self.delegate = self
self.direction = [.left, .right]
self.numberOfTouchesRequired = 2
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

View file

@ -1141,7 +1141,7 @@ private final class StickerVideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init() {
self.contentContainerNode = ASDisplayNode()
@ -1161,9 +1161,9 @@ private final class StickerVideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -1221,8 +1221,8 @@ private final class StickerVideoDecoration: UniversalVideoDecoration {
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
@ -1237,7 +1237,7 @@ private final class StickerVideoDecoration: UniversalVideoDecoration {
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}

View file

@ -27,7 +27,7 @@ let package = Package(
publicHeadersPath: "Public",
cSettings: [
.headerSearchPath("Public"),
.unsafeFlags(["-I../../../../core-xprojects/ffmpeg/build/ffmpeg/include"])
.headerSearchPath("SharedHeaders/ffmpeg/include"),
]),
]
)

View file

@ -14,7 +14,7 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init() {
self.contentContainerNode = ASDisplayNode()
@ -34,9 +34,9 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -94,8 +94,8 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration {
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
@ -110,7 +110,7 @@ public final class GalleryVideoDecoration: UniversalVideoDecoration {
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}

View file

@ -604,6 +604,8 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte
private var hiddenMediaManagerIndex: Int?
private var messageRemovedDisposable: Disposable?
private var isNativePictureInPictureActiveDisposable: Disposable?
init(context: AccountContext, overlayController: OverlayMediaController, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (PictureInPictureContentImpl) -> Void, didEnd: @escaping (PictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) {
self.overlayController = overlayController
@ -617,30 +619,84 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte
super.init()
let contentDelegate = PlaybackDelegate(node: self.node)
self.contentDelegate = contentDelegate
if let videoLayer = videoNode.getVideoLayer() {
let contentDelegate = PlaybackDelegate(node: self.node)
self.contentDelegate = contentDelegate
let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: contentDelegate))
self.pictureInPictureController = pictureInPictureController
contentDelegate.pictureInPictureController = pictureInPictureController
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false
pictureInPictureController.requiresLinearPlayback = !canSkip
pictureInPictureController.delegate = self
self.pictureInPictureController = pictureInPictureController
let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in
guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else {
return
}
if pictureInPictureController.isPictureInPicturePossible {
strongSelf.pictureInPictureTimer?.invalidate()
strongSelf.pictureInPictureTimer = nil
pictureInPictureController.startPictureInPicture()
}
}, queue: .mainQueue())
self.pictureInPictureTimer = timer
timer.start()
} else {
var currentIsNativePictureInPictureActive = false
self.isNativePictureInPictureActiveDisposable = (videoNode.isNativePictureInPictureActive
|> deliverOnMainQueue).startStrict(next: { [weak self] isNativePictureInPictureActive in
guard let self else {
return
}
if currentIsNativePictureInPictureActive == isNativePictureInPictureActive {
return
}
currentIsNativePictureInPictureActive = isNativePictureInPictureActive
if isNativePictureInPictureActive {
Queue.mainQueue().after(0.0, { [weak self] in
guard let self else {
return
}
self.willBegin(self)
if let overlayController = self.overlayController {
overlayController.setPictureInPictureContentHidden(content: self, isHidden: true)
}
self.didEnd(self)
})
} else {
self.expand { [weak self] in
guard let self else {
return
}
let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoNode.getVideoLayer()!, playbackDelegate: contentDelegate))
self.pictureInPictureController = pictureInPictureController
contentDelegate.pictureInPictureController = pictureInPictureController
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false
pictureInPictureController.requiresLinearPlayback = !canSkip
pictureInPictureController.delegate = self
self.pictureInPictureController = pictureInPictureController
let timer = SwiftSignalKit.Timer(timeout: 0.005, repeat: true, completion: { [weak self] in
guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else {
return
}
if pictureInPictureController.isPictureInPicturePossible {
strongSelf.pictureInPictureTimer?.invalidate()
strongSelf.pictureInPictureTimer = nil
self.didExpand = true
pictureInPictureController.startPictureInPicture()
}
}, queue: .mainQueue())
self.pictureInPictureTimer = timer
timer.start()
if let overlayController = self.overlayController {
overlayController.setPictureInPictureContentHidden(content: self, isHidden: false)
self.node.alpha = 0.02
}
guard let overlayController = self.overlayController else {
return
}
overlayController.removePictureInPictureContent(content: self)
self.node.canAttachContent = false
if self.didExpand {
return
}
self.node.continuePlayingWithoutSound()
}
}
})
let _ = videoNode.enterNativePictureInPicture()
}
if let hiddenMedia = hiddenMedia {
self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(Signal<(MessageId, Media)?, NoError>.single(hiddenMedia)
@ -676,6 +732,7 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte
deinit {
self.messageRemovedDisposable?.dispose()
self.isNativePictureInPictureActiveDisposable?.dispose()
self.pictureInPictureTimer?.invalidate()
self.node.setCanPlaybackWithoutHierarchy(false)
@ -743,10 +800,6 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte
}
completionHandler(true)
/*Queue.mainQueue().after(0.2, {
self?.node.canAttachContent = false
})*/
}
}
}
@ -1186,7 +1239,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
videoScale = 2.0
}
let videoSize = CGSize(width: item.content.dimensions.width * videoScale, height: item.content.dimensions.height * videoScale)
videoNode.updateLayout(size: videoSize, transition: .immediate)
let actualVideoSize = CGSize(width: item.content.dimensions.width, height: item.content.dimensions.height)
videoNode.updateLayout(size: videoSize, actualSize: actualVideoSize, transition: .immediate)
videoNode.ownsContentNodeUpdated = { [weak self] value in
if let strongSelf = self {
strongSelf.updateDisplayPlaceholder(!value)
@ -1294,7 +1348,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
/*self.moreButtonStateDisposable.set(combineLatest(queue: .mainQueue(),
self.playbackRatePromise.get(),
self.isShowingContextMenuPromise.get()
).start(next: { [weak self] playbackRate, isShowingContextMenu in
@ -1333,7 +1387,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
}
}
}))
}))*/
self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus)
|> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in
@ -2305,6 +2359,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
let playbackRate = self.playbackRate
if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported(), isNativePictureInPictureSupported {
self.disablePictureInPicturePlaceholder = true
let overlayVideoNode = UniversalVideoNode(accountId: self.context.account.id, postbox: self.context.account.postbox, audioSession: self.context.sharedContext.mediaManager.audioSession, manager: self.context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .overlay)
@ -2837,7 +2892,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil && !(item.content is HLSVideoContent) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveVideo, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
f(.default)

View file

@ -623,6 +623,8 @@ private final class PendingInAppPurchaseState: Codable {
case untilDate
case stars
case users
case text
case entities
}
enum PurposeType: Int32 {
@ -641,7 +643,7 @@ private final class PendingInAppPurchaseState: Codable {
case upgrade
case restore
case gift(peerId: EnginePeer.Id)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, text: String?, entities: [MessageTextEntity]?)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32)
case stars(count: Int64)
case starsGift(peerId: EnginePeer.Id, count: Int64)
@ -665,7 +667,9 @@ private final class PendingInAppPurchaseState: Codable {
case .giftCode:
self = .giftCode(
peerIds: try container.decode([Int64].self, forKey: .peers).map { EnginePeer.Id($0) },
boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) })
boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }),
text: try container.decodeIfPresent(String.self, forKey: .text),
entities: try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities)
)
case .giveaway:
self = .giveaway(
@ -718,10 +722,12 @@ private final class PendingInAppPurchaseState: Codable {
case let .gift(peerId):
try container.encode(PurposeType.gift.rawValue, forKey: .type)
try container.encode(peerId.toInt64(), forKey: .peer)
case let .giftCode(peerIds, boostPeer):
case let .giftCode(peerIds, boostPeer, text, entities):
try container.encode(PurposeType.giftCode.rawValue, forKey: .type)
try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers)
try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer)
try container.encodeIfPresent(text, forKey: .text)
try container.encodeIfPresent(entities, forKey: .entities)
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate):
try container.encode(PurposeType.giveaway.rawValue, forKey: .type)
try container.encode(boostPeer.toInt64(), forKey: .boostPeer)
@ -764,8 +770,8 @@ private final class PendingInAppPurchaseState: Codable {
self = .restore
case let .gift(peerId, _, _):
self = .gift(peerId: peerId)
case let .giftCode(peerIds, boostPeer, _, _):
self = .giftCode(peerIds: peerIds, boostPeer: boostPeer)
case let .giftCode(peerIds, boostPeer, _, _, text, entities):
self = .giftCode(peerIds: peerIds, boostPeer: boostPeer, text: text, entities: entities)
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _):
self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate)
case let .stars(count, _, _):
@ -788,8 +794,8 @@ private final class PendingInAppPurchaseState: Codable {
return .restore
case let .gift(peerId):
return .gift(peerId: peerId, currency: currency, amount: amount)
case let .giftCode(peerIds, boostPeer):
return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount)
case let .giftCode(peerIds, boostPeer, text, entities):
return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount, text: text, entities: entities)
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate):
return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
case let .stars(count):

View file

@ -23,10 +23,8 @@ let package = Package(
publicHeadersPath: "PublicHeaders",
cSettings: [
.headerSearchPath("PublicHeaders"),
.unsafeFlags([
"-I../../../../core-xprojects/openssl/build/openssl/include",
"-I../EncryptionProvider/PublicHeaders"
])
.headerSearchPath("SharedHeaders/openssl/include"),
.headerSearchPath("SharedHeaders/EncryptionProvider"),
]),
]
)

View file

@ -28,8 +28,8 @@ let package = Package(
cSettings: [
.headerSearchPath("PublicHeaders"),
.headerSearchPath("PublicHeaders/OpusBinding"),
.headerSearchPath("SharedHeaders/libopus/include"),
.headerSearchPath("Sources"),
.unsafeFlags(["-I../../../../core-xprojects/libopus/build/libopus/include"])
]),
]
)

View file

@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 226.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed:
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro:
self.buttonSize = 75.0
self.horizontalSecond = 103.0
self.horizontalThird = 206.0
@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout {
self.topOffset = 294.0
self.biometricsOffset = 30.0
self.deleteOffset = 20.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
self.buttonSize = 85.0
self.horizontalSecond = 115.0
self.horizontalThird = 230.0
@ -151,11 +151,11 @@ public struct PasscodeLayout {
self.titleOffset = 112.0
self.subtitleOffset = -6.0
self.inputFieldOffset = 156.0
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed:
case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed, .iPhone14ProMaxZoomed, .iPhone16Pro:
self.titleOffset = 162.0
self.subtitleOffset = 0.0
self.inputFieldOffset = 206.0
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax:
case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone16ProMax:
self.titleOffset = 180.0
self.subtitleOffset = 0.0
self.inputFieldOffset = 226.0

View file

@ -674,6 +674,28 @@ public final class MediaBox {
return self.resourceData(id: resource.id, size: size, in: range, mode: mode, notifyAboutIncomplete: notifyAboutIncomplete, attemptSynchronously: attemptSynchronously)
}
public func internal_resourceData(id: MediaResourceId, size: Int64, in range: Range<Int64>) -> (file: ManagedFile, length: Int)? {
let paths = self.storePathsForId(id)
self.timeBasedCleanup.touch(paths: [
paths.complete
])
if let file = ManagedFile(queue: nil, path: paths.complete, mode: .read), let completeSize = file.getSize() {
let clippedLowerBound = min(completeSize, max(0, range.lowerBound))
let clippedUpperBound = min(completeSize, max(0, range.upperBound))
if clippedLowerBound < clippedUpperBound && (clippedUpperBound - clippedLowerBound) <= 64 * 1024 * 1024 {
let _ = file.seek(position: clippedLowerBound)
return (file, Int(clippedUpperBound - clippedLowerBound))
} else {
return nil
}
} else {
let tempManager = MediaBoxFileManager(queue: nil)
return MediaBoxPartialFile.internal_extractPartialData(manager: tempManager, path: paths.partial, metaPath: paths.partial + ".meta", range: range)
}
}
public func resourceData(id: MediaResourceId, size: Int64, in range: Range<Int64>, mode: ResourceDataRangeMode = .complete, notifyAboutIncomplete: Bool = false, attemptSynchronously: Bool = false) -> Signal<(Data, Bool), NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()

View file

@ -92,6 +92,20 @@ final class MediaBoxPartialFile {
return fd.readData(count: Int(clippedRange.upperBound - clippedRange.lowerBound))
}
static func internal_extractPartialData(manager: MediaBoxFileManager, path: String, metaPath: String, range: Range<Int64>) -> (file: ManagedFile, length: Int)? {
guard let fd = ManagedFile(queue: nil, path: path, mode: .read) else {
return nil
}
guard let fileMap = try? MediaBoxFileMap.read(manager: manager, path: metaPath) else {
return nil
}
guard let clippedRange = fileMap.contains(range) else {
return nil
}
let _ = fd.seek(position: Int64(clippedRange.lowerBound))
return (fd, Int(clippedRange.upperBound - clippedRange.lowerBound))
}
var storedSize: Int64 {
assert(self.queue.isCurrent())
return self.fileMap.sum

View file

@ -1356,7 +1356,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
return
}
let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount, text: nil, entities: nil)
quantity = Int32(state.peers.count)
storeProduct = selectedProduct.storeProduct
case .starsGiveaway:

View file

@ -626,7 +626,7 @@ private final class VideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init() {
self.contentContainerNode = ASDisplayNode()
@ -646,9 +646,9 @@ private final class VideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -706,8 +706,8 @@ private final class VideoDecoration: UniversalVideoDecoration {
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
@ -722,7 +722,7 @@ private final class VideoDecoration: UniversalVideoDecoration {
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}

View file

@ -910,7 +910,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
if self.source == .profile || self.source == .attachMenu, let peerId = self.peerIds.first {
purpose = .gift(peerId: peerId, currency: currency, amount: amount)
} else {
purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount)
purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount, text: nil, entities: nil)
quantity = Int32(self.peerIds.count)
}

View file

@ -467,7 +467,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) }
dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) }
dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) }
dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) }
dict[-75955309] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) }
dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) }
dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) }
dict[494149367] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsGift($0) }
@ -551,8 +551,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) }
dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) }
dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) }
dict[1737240073] = { return Api.MessageAction.parse_messageActionGiftCode($0) }
dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) }
dict[1456486804] = { return Api.MessageAction.parse_messageActionGiftCode($0) }
dict[1818391802] = { return Api.MessageAction.parse_messageActionGiftPremium($0) }
dict[1171632161] = { return Api.MessageAction.parse_messageActionGiftStars($0) }
dict[-1475391004] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) }
dict[-2015170219] = { return Api.MessageAction.parse_messageActionGiveawayResults($0) }

View file

@ -711,7 +711,7 @@ public extension Api {
public extension Api {
indirect enum InputStorePaymentPurpose: TypeConstructorDescription {
case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64)
case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64)
case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64, message: Api.TextWithEntities?)
case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case inputStorePaymentPremiumSubscription(flags: Int32)
case inputStorePaymentStarsGift(userId: Api.InputUser, stars: Int64, currency: String, amount: Int64)
@ -728,9 +728,9 @@ public extension Api {
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
break
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount):
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message):
if boxed {
buffer.appendInt32(-1551868097)
buffer.appendInt32(-75955309)
}
serializeInt32(flags, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
@ -741,6 +741,7 @@ public extension Api {
if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)}
serializeString(currency, buffer: buffer, boxed: false)
serializeInt64(amount, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)}
break
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount):
if boxed {
@ -818,8 +819,8 @@ public extension Api {
switch self {
case .inputStorePaymentGiftPremium(let userId, let currency, let amount):
return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount):
return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message):
return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any), ("message", message as Any)])
case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount):
return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)])
case .inputStorePaymentPremiumSubscription(let flags):
@ -867,13 +868,18 @@ public extension Api {
_4 = parseString(reader)
var _5: Int64?
_5 = reader.readInt64()
var _6: Api.TextWithEntities?
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_6 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!)
let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!, message: _6)
}
else {
return nil

View file

@ -986,8 +986,8 @@ public extension Api {
case messageActionEmpty
case messageActionGameScore(gameId: Int64, score: Int32)
case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32)
case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?)
case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?)
case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?)
case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?)
case messageActionGiftStars(flags: Int32, currency: String, amount: Int64, stars: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?)
case messageActionGiveawayLaunch(flags: Int32, stars: Int64?)
case messageActionGiveawayResults(flags: Int32, winnersCount: Int32, unclaimedCount: Int32)
@ -1141,9 +1141,9 @@ public extension Api {
toId.serialize(buffer, true)
serializeInt32(distance, buffer: buffer, boxed: false)
break
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount):
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message):
if boxed {
buffer.appendInt32(1737240073)
buffer.appendInt32(1456486804)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {boostPeer!.serialize(buffer, true)}
@ -1153,10 +1153,11 @@ public extension Api {
if Int(flags) & Int(1 << 2) != 0 {serializeInt64(amount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 3) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {message!.serialize(buffer, true)}
break
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount):
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message):
if boxed {
buffer.appendInt32(-935499028)
buffer.appendInt32(1818391802)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeString(currency, buffer: buffer, boxed: false)
@ -1164,6 +1165,7 @@ public extension Api {
serializeInt32(months, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)}
break
case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId):
if boxed {
@ -1439,10 +1441,10 @@ public extension Api {
return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)])
case .messageActionGeoProximityReached(let fromId, let toId, let distance):
return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)])
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount):
return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)])
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount):
return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)])
case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message):
return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)])
case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message):
return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)])
case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId):
return ("messageActionGiftStars", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("stars", stars as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("transactionId", transactionId as Any)])
case .messageActionGiveawayLaunch(let flags, let stars):
@ -1718,6 +1720,10 @@ public extension Api {
if Int(_1!) & Int(1 << 3) != 0 {_7 = parseString(reader) }
var _8: Int64?
if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() }
var _9: Api.TextWithEntities?
if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() {
_9 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
} }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
let _c3 = _3 != nil
@ -1726,8 +1732,9 @@ public extension Api {
let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil
let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil
let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8)
let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8, message: _9)
}
else {
return nil
@ -1746,14 +1753,19 @@ public extension Api {
if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) }
var _6: Int64?
if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() }
var _7: Api.TextWithEntities?
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities
} }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6)
let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6, message: _7)
}
else {
return nil

View file

@ -36,13 +36,13 @@ final class VideoChatActionButtonComponent: Component {
case leave
}
case audio(audio: Audio)
case audio(audio: Audio, isEnabled: Bool)
case video(isActive: Bool)
case leave
fileprivate var iconType: IconType {
switch self {
case let .audio(audio):
case let .audio(audio, _):
let mappedAudio: IconType.Audio
switch audio {
case .none, .builtin, .speaker:
@ -136,14 +136,16 @@ final class VideoChatActionButtonComponent: Component {
let titleText: String
let backgroundColor: UIColor
let iconDiameter: CGFloat
var isEnabled: Bool = true
switch component.content {
case let .audio(audio):
case let .audio(audio, isEnabledValue):
var isActive = false
switch audio {
case .none, .builtin:
titleText = component.strings.Call_Speaker
case .speaker:
isActive = true
isEnabled = isEnabledValue
isActive = isEnabledValue
titleText = component.strings.Call_Speaker
case .headphones:
titleText = component.strings.Call_Audio
@ -276,8 +278,11 @@ final class VideoChatActionButtonComponent: Component {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
transition.setAlpha(view: iconView, alpha: isEnabled ? 1.0 : 0.6)
}
self.isEnabled = isEnabled
return size
}
}

View file

@ -11,6 +11,7 @@ import SwiftSignalKit
import MetalEngine
import CallScreen
import AvatarNode
import ContextUI
final class VideoChatParticipantThumbnailComponent: Component {
let call: PresentationGroupCall
@ -21,6 +22,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
let isSpeaking: Bool
let interfaceOrientation: UIInterfaceOrientation
let action: (() -> Void)?
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
init(
call: PresentationGroupCall,
@ -30,7 +32,8 @@ final class VideoChatParticipantThumbnailComponent: Component {
isSelected: Bool,
isSpeaking: Bool,
interfaceOrientation: UIInterfaceOrientation,
action: (() -> Void)?
action: (() -> Void)?,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
) {
self.call = call
self.theme = theme
@ -40,6 +43,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.isSpeaking = isSpeaking
self.interfaceOrientation = interfaceOrientation
self.action = action
self.contextAction = contextAction
}
static func ==(lhs: VideoChatParticipantThumbnailComponent, rhs: VideoChatParticipantThumbnailComponent) -> Bool {
@ -64,6 +68,12 @@ final class VideoChatParticipantThumbnailComponent: Component {
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true
}
@ -79,7 +89,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
}
}
final class View: HighlightTrackingButton {
final class View: ContextControllerSourceView {
private static let selectedBorderImage: UIImage? = {
return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: UIColor.white, strokeWidth: 2.0)?.withRenderingMode(.alwaysTemplate)
}()
@ -88,6 +98,10 @@ final class VideoChatParticipantThumbnailComponent: Component {
private weak var componentState: EmptyComponentState?
private var isUpdating: Bool = false
private let extractedContainerView: ContextExtractedContentContainingView
private let backgroundLayer: SimpleLayer
private var avatarNode: AvatarNode?
private let title = ComponentView<Empty>()
private let muteStatus = ComponentView<Empty>()
@ -101,13 +115,30 @@ final class VideoChatParticipantThumbnailComponent: Component {
private var videoSpec: VideoSpec?
override init(frame: CGRect) {
self.extractedContainerView = ContextExtractedContentContainingView()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.backgroundColor = UIColor(rgb: 0x1C1C1E).cgColor
super.init(frame: frame)
//TODO:release optimize
self.clipsToBounds = true
self.layer.cornerRadius = 10.0
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.extractedContainerView.contentView.layer.addSublayer(self.backgroundLayer)
self.extractedContainerView.contentView.clipsToBounds = true
self.extractedContainerView.contentView.layer.cornerRadius = 10.0
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture)
}
}
required init?(coder: NSCoder) {
@ -118,11 +149,13 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.videoDisposable?.dispose()
}
@objc private func pressed() {
guard let component = self.component, let action = component.action else {
return
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let component = self.component, let action = component.action else {
return
}
action()
}
action()
}
func update(component: VideoChatParticipantThumbnailComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
@ -131,10 +164,6 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.isUpdating = false
}
if self.component == nil {
self.backgroundColor = UIColor(rgb: 0x1C1C1E)
}
let previousComponent = self.component
let wasSpeaking = previousComponent?.isSpeaking ?? false
@ -156,6 +185,14 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.component = component
self.componentState = state
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
@ -163,7 +200,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
avatarNode.isUserInteractionEnabled = false
self.avatarNode = avatarNode
self.addSubview(avatarNode.view)
self.extractedContainerView.contentView.addSubview(avatarNode.view)
}
let avatarSize = CGSize(width: 50.0, height: 50.0)
@ -188,7 +225,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
let muteStatusFrame = CGRect(origin: CGPoint(x: availableSize.width + 5.0 - muteStatusSize.width, y: availableSize.height + 5.0 - muteStatusSize.height), size: muteStatusSize)
if let muteStatusView = self.muteStatus.view as? VideoChatMuteIconComponent.View {
if muteStatusView.superview == nil {
self.addSubview(muteStatusView)
self.extractedContainerView.contentView.addSubview(muteStatusView)
}
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
@ -208,7 +245,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
titleView.isUserInteractionEnabled = false
self.addSubview(titleView)
self.extractedContainerView.contentView.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
@ -222,7 +259,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
videoBackgroundLayer = SimpleLayer()
videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor
self.videoBackgroundLayer = videoBackgroundLayer
self.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer)
self.extractedContainerView.contentView.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer)
videoBackgroundLayer.isHidden = true
}
@ -232,8 +269,8 @@ final class VideoChatParticipantThumbnailComponent: Component {
} else {
videoLayer = PrivateCallVideoLayer()
self.videoLayer = videoLayer
self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)
self.extractedContainerView.contentView.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.extractedContainerView.contentView.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)
videoLayer.blurredLayer.opacity = 0.25
@ -346,7 +383,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
selectedBorderView = UIImageView()
self.selectedBorderView = selectedBorderView
selectedBorderView.alpha = 0.0
self.addSubview(selectedBorderView)
self.extractedContainerView.contentView.addSubview(selectedBorderView)
selectedBorderView.image = View.selectedBorderImage
selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize)
@ -438,6 +475,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
let speakingParticipants: Set<EnginePeer.Id>
let interfaceOrientation: UIInterfaceOrientation
let updateSelectedParticipant: (Participant.Key) -> Void
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
init(
call: PresentationGroupCall,
@ -446,7 +484,8 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
selectedParticipant: Participant.Key?,
speakingParticipants: Set<EnginePeer.Id>,
interfaceOrientation: UIInterfaceOrientation,
updateSelectedParticipant: @escaping (Participant.Key) -> Void
updateSelectedParticipant: @escaping (Participant.Key) -> Void,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
) {
self.call = call
self.theme = theme
@ -455,6 +494,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
self.speakingParticipants = speakingParticipants
self.interfaceOrientation = interfaceOrientation
self.updateSelectedParticipant = updateSelectedParticipant
self.contextAction = contextAction
}
static func ==(lhs: VideoChatExpandedParticipantThumbnailsComponent, rhs: VideoChatExpandedParticipantThumbnailsComponent) -> Bool {
@ -476,6 +516,9 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true
}
@ -617,7 +660,8 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
return
}
component.updateSelectedParticipant(participantKey)
}
},
contextAction: component.contextAction
)),
environment: {},
containerSize: itemFrame.size

View file

@ -16,13 +16,19 @@ final class VideoChatMuteIconComponent: Component {
let color: UIColor
let content: Content
let shadowColor: UIColor?
let shadowBlur: CGFloat
init(
color: UIColor,
content: Content
content: Content,
shadowColor: UIColor? = nil,
shadowBlur: CGFloat = 0.0
) {
self.color = color
self.content = content
self.shadowColor = shadowColor
self.shadowBlur = shadowBlur
}
static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool {
@ -32,6 +38,12 @@ final class VideoChatMuteIconComponent: Component {
if lhs.content != rhs.content {
return false
}
if lhs.shadowColor != rhs.shadowColor {
return false
}
if lhs.shadowBlur != rhs.shadowBlur {
return false
}
return true
}
@ -75,9 +87,9 @@ final class VideoChatMuteIconComponent: Component {
}
let animationSize = availableSize
let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize))
let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)).insetBy(dx: -component.shadowBlur, dy: -component.shadowBlur)
transition.setFrame(view: icon.view, frame: animationFrame)
icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color), animated: !transition.animation.isImmediate)
icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color, shadowColor: component.shadowColor, shadowBlur: component.shadowBlur), animated: !transition.animation.isImmediate)
} else {
if let icon = self.icon {
self.icon = nil
@ -97,7 +109,9 @@ final class VideoChatMuteIconComponent: Component {
transition: transition,
component: AnyComponent(BundleIconComponent(
name: "Call/StatusScreen",
tintColor: component.color
tintColor: component.color,
shadowColor: component.shadowColor,
shadowBlur: component.shadowBlur
)),
environment: {},
containerSize: availableSize

View file

@ -12,6 +12,9 @@ import AccountContext
import SwiftSignalKit
import DirectMediaImageCache
import FastBlur
import ContextUI
import ComponentDisplayAdapters
import AvatarNode
private func blurredAvatarImage(_ dataImage: UIImage) -> UIImage? {
let imageContextSize = CGSize(width: 64.0, height: 64.0)
@ -35,6 +38,7 @@ private let activityBorderImage: UIImage = {
}()
final class VideoChatParticipantVideoComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let call: PresentationGroupCall
let participant: GroupCallParticipantsContext.Participant
@ -47,8 +51,12 @@ final class VideoChatParticipantVideoComponent: Component {
let controlInsets: UIEdgeInsets
let interfaceOrientation: UIInterfaceOrientation
let action: (() -> Void)?
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
let activatePinch: ((PinchSourceContainerNode) -> Void)?
let deactivatedPinch: (() -> Void)?
init(
theme: PresentationTheme,
strings: PresentationStrings,
call: PresentationGroupCall,
participant: GroupCallParticipantsContext.Participant,
@ -60,8 +68,12 @@ final class VideoChatParticipantVideoComponent: Component {
contentInsets: UIEdgeInsets,
controlInsets: UIEdgeInsets,
interfaceOrientation: UIInterfaceOrientation,
action: (() -> Void)?
action: (() -> Void)?,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?,
activatePinch: ((PinchSourceContainerNode) -> Void)?,
deactivatedPinch: (() -> Void)?
) {
self.theme = theme
self.strings = strings
self.call = call
self.participant = participant
@ -74,6 +86,9 @@ final class VideoChatParticipantVideoComponent: Component {
self.controlInsets = controlInsets
self.interfaceOrientation = interfaceOrientation
self.action = action
self.contextAction = contextAction
self.activatePinch = activatePinch
self.deactivatedPinch = deactivatedPinch
}
static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool {
@ -107,6 +122,15 @@ final class VideoChatParticipantVideoComponent: Component {
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
if (lhs.activatePinch == nil) != (rhs.activatePinch == nil) {
return false
}
if (lhs.deactivatedPinch == nil) != (rhs.deactivatedPinch == nil) {
return false
}
return true
}
@ -144,7 +168,7 @@ final class VideoChatParticipantVideoComponent: Component {
}
}
final class View: HighlightTrackingButton {
final class View: ContextControllerSourceView {
private var component: VideoChatParticipantVideoComponent?
private weak var componentState: EmptyComponentState?
private var isUpdating: Bool = false
@ -158,6 +182,8 @@ final class VideoChatParticipantVideoComponent: Component {
private var blurredAvatarDisposable: Disposable?
private var blurredAvatarView: UIImageView?
private let pinchContainerNode: PinchSourceContainerNode
private let extractedContainerView: ContextExtractedContentContainingView
private var videoSource: AdaptedCallVideoSource?
private var videoDisposable: Disposable?
private var videoBackgroundLayer: SimpleLayer?
@ -173,16 +199,44 @@ final class VideoChatParticipantVideoComponent: Component {
override init(frame: CGRect) {
self.backgroundGradientView = UIImageView()
self.pinchContainerNode = PinchSourceContainerNode()
self.extractedContainerView = ContextExtractedContentContainingView()
super.init(frame: frame)
self.addSubview(self.backgroundGradientView)
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.pinchContainerNode.view)
self.pinchContainerNode.contentNode.view.addSubview(self.backgroundGradientView)
//TODO:release optimize
self.clipsToBounds = true
self.layer.cornerRadius = 10.0
self.pinchContainerNode.contentNode.view.layer.cornerRadius = 10.0
self.pinchContainerNode.contentNode.view.clipsToBounds = true
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.pinchContainerNode.activate = { [weak self] sourceNode in
guard let self, let component = self.component else {
return
}
component.activatePinch?(sourceNode)
}
self.pinchContainerNode.animatedOut = { [weak self] in
guard let self, let component = self.component else {
return
}
component.deactivatedPinch?()
}
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction?(EnginePeer(component.participant.peer), self.extractedContainerView, gesture)
}
}
required init?(coder: NSCoder) {
@ -194,11 +248,13 @@ final class VideoChatParticipantVideoComponent: Component {
self.blurredAvatarDisposable?.dispose()
}
@objc private func pressed() {
guard let component = self.component, let action = component.action else {
return
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let component = self.component, let action = component.action else {
return
}
action()
}
action()
}
func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
@ -211,6 +267,21 @@ final class VideoChatParticipantVideoComponent: Component {
self.component = component
self.componentState = state
self.isGestureEnabled = !component.isExpanded
self.pinchContainerNode.isPinchGestureEnabled = component.activatePinch != nil
transition.setPosition(view: self.pinchContainerNode.view, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.pinchContainerNode.view, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.pinchContainerNode.update(size: availableSize, transition: transition.containedViewLayoutTransition)
transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
transition.setFrame(view: self.pinchContainerNode.contentNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: self.backgroundGradientView, frame: CGRect(origin: CGPoint(), size: availableSize))
let alphaTransition: ComponentTransition
@ -229,14 +300,10 @@ final class VideoChatParticipantVideoComponent: Component {
let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0
let nameColor = component.participant.peer.nameColor ?? .blue
let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true)
if previousComponent == nil {
self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: [
nameColors.main.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 1.3),
nameColors.main.withMultiplied(hue: 1.0, saturation: 1.2, brightness: 1.0)
], locations: [0.0, 1.0], direction: .vertical)
let colors = calculateAvatarColors(context: component.call.accountContext, explicitColorIndex: nil, peerId: component.participant.peer.id, nameColor: component.participant.peer.nameColor, icon: .none, theme: component.theme)
self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: colors.reversed(), locations: [0.0, 1.0], direction: .vertical)
}
if let smallProfileImage = component.participant.peer.smallProfileImage {
@ -249,7 +316,7 @@ final class VideoChatParticipantVideoComponent: Component {
blurredAvatarView = UIImageView()
blurredAvatarView.contentMode = .scaleAspectFill
self.blurredAvatarView = blurredAvatarView
self.insertSubview(blurredAvatarView, aboveSubview: self.backgroundGradientView)
self.pinchContainerNode.contentNode.view.insertSubview(blurredAvatarView, aboveSubview: self.backgroundGradientView)
blurredAvatarView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
@ -292,7 +359,9 @@ final class VideoChatParticipantVideoComponent: Component {
transition: transition,
component: AnyComponent(VideoChatMuteIconComponent(
color: .white,
content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking)
content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking),
shadowColor: UIColor(white: 0.0, alpha: 0.7),
shadowBlur: 8.0
)),
environment: {},
containerSize: CGSize(width: 36.0, height: 36.0)
@ -305,14 +374,8 @@ final class VideoChatParticipantVideoComponent: Component {
}
if let muteStatusView = self.muteStatus.view {
if muteStatusView.superview == nil {
self.addSubview(muteStatusView)
self.pinchContainerNode.contentNode.view.addSubview(muteStatusView)
muteStatusView.alpha = controlsAlpha
//TODO:release
muteStatusView.layer.shadowOpacity = 0.7
muteStatusView.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
muteStatusView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
muteStatusView.layer.shadowRadius = 8.0
}
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
@ -320,31 +383,29 @@ final class VideoChatParticipantVideoComponent: Component {
alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha)
}
let titleInnerInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.semibold(16.0), textColor: .white))
text: .plain(NSAttributedString(string: component.participant.peer.debugDisplayTitle, font: Font.semibold(16.0), textColor: .white)),
insets: titleInnerInsets,
textShadowColor: UIColor(white: 0.0, alpha: 0.7),
textShadowBlur: 8.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 8.0 * 2.0 - 4.0, height: 100.0)
)
let titleFrame: CGRect
if component.isExpanded {
titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height), size: titleSize)
titleFrame = CGRect(origin: CGPoint(x: 36.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height + titleInnerInsets.top), size: titleSize)
} else {
titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height), size: titleSize)
titleFrame = CGRect(origin: CGPoint(x: 29.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height + titleInnerInsets.top + 1.0), size: titleSize)
}
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
self.pinchContainerNode.contentNode.view.addSubview(titleView)
titleView.alpha = controlsAlpha
//TODO:release
titleView.layer.shadowOpacity = 0.7
titleView.layer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor
titleView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
titleView.layer.shadowRadius = 8.0
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
@ -377,9 +438,9 @@ final class VideoChatParticipantVideoComponent: Component {
videoBackgroundLayer.opacity = 0.0
self.videoBackgroundLayer = videoBackgroundLayer
if let blurredAvatarView = self.blurredAvatarView {
self.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer)
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer)
} else {
self.layer.insertSublayer(videoBackgroundLayer, above: self.backgroundGradientView.layer)
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: self.backgroundGradientView.layer)
}
videoBackgroundLayer.isHidden = true
}
@ -391,8 +452,8 @@ final class VideoChatParticipantVideoComponent: Component {
videoLayer = PrivateCallVideoLayer()
self.videoLayer = videoLayer
videoLayer.opacity = 0.0
self.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)
videoLayer.blurredLayer.opacity = 0.0
@ -537,7 +598,7 @@ final class VideoChatParticipantVideoComponent: Component {
if videoStatusView.superview == nil {
videoStatusView.isUserInteractionEnabled = false
videoStatusView.alpha = 0.0
self.addSubview(videoStatusView)
self.pinchContainerNode.contentNode.view.addSubview(videoStatusView)
}
videoStatusTransition.setFrame(view: videoStatusView, frame: CGRect(origin: CGPoint(), size: availableSize))
videoAlphaTransition.setAlpha(view: videoStatusView, alpha: 1.0)
@ -557,7 +618,7 @@ final class VideoChatParticipantVideoComponent: Component {
self.loadingEffectView = loadingEffectView
loadingEffectView.alpha = 0.0
loadingEffectView.isUserInteractionEnabled = false
self.addSubview(loadingEffectView)
self.pinchContainerNode.contentNode.view.addSubview(loadingEffectView)
if let referenceLocation = self.referenceLocation {
self.updateHorizontalReferenceLocation(containerWidth: referenceLocation.containerWidth, positionX: referenceLocation.positionX, transition: .immediate)
}
@ -578,7 +639,7 @@ final class VideoChatParticipantVideoComponent: Component {
} else {
activityBorderView = UIImageView()
self.activityBorderView = activityBorderView
self.addSubview(activityBorderView)
self.pinchContainerNode.contentNode.view.addSubview(activityBorderView)
activityBorderView.image = activityBorderImage
activityBorderView.tintColor = UIColor(rgb: 0x33C758)

View file

@ -10,6 +10,7 @@ import SwiftSignalKit
import MultilineTextComponent
import TelegramPresentationData
import PeerListItemComponent
import ContextUI
final class VideoChatParticipantsComponent: Component {
struct Layout: Equatable {
@ -645,6 +646,10 @@ final class VideoChatParticipantsComponent: Component {
private var appliedGridIsEmpty: Bool = true
private var isPinchToZoomActive: Bool = false
private var stopRequestingNonCentralVideo: Bool = false
private var stopRequestingNonCentralVideoTimer: Foundation.Timer?
private var currentLoadMoreToken: String?
private var mainScrollViewEventCycleState: EventCycleState?
@ -719,6 +724,10 @@ final class VideoChatParticipantsComponent: Component {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.stopRequestingNonCentralVideoTimer?.invalidate()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let component = self.component else {
return nil
@ -986,7 +995,10 @@ final class VideoChatParticipantsComponent: Component {
var itemControlInsets: UIEdgeInsets
if isItemExpanded {
itemControlInsets = itemContentInsets
itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0)
if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden {
} else {
itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0)
}
} else {
itemControlInsets = itemContentInsets
}
@ -1003,6 +1015,7 @@ final class VideoChatParticipantsComponent: Component {
let _ = itemView.view.update(
transition: itemTransition,
component: AnyComponent(VideoChatParticipantVideoComponent(
theme: component.theme,
strings: component.strings,
call: component.call,
participant: videoParticipant.participant,
@ -1010,7 +1023,7 @@ final class VideoChatParticipantsComponent: Component {
isPresentation: videoParticipant.isPresentation,
isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id),
isExpanded: isItemExpanded,
isUIHidden: isItemUIHidden,
isUIHidden: isItemUIHidden || self.isPinchToZoomActive,
contentInsets: itemContentInsets,
controlInsets: itemControlInsets,
interfaceOrientation: component.interfaceOrientation,
@ -1032,7 +1045,31 @@ final class VideoChatParticipantsComponent: Component {
component.updateMainParticipant(videoParticipantKey, nil)
}
}
}
},
contextAction: !isItemExpanded ? { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, sourceView, gesture)
} : nil,
activatePinch: isItemExpanded ? { [weak self] sourceNode in
guard let self, let component = self.component else {
return
}
self.isPinchToZoomActive = true
self.state?.updated(transition: .immediate, isLocal: true)
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
return UIScreen.main.bounds
})
component.call.accountContext.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController)
} : nil,
deactivatedPinch: isItemExpanded ? { [weak self] in
guard let self else {
return
}
self.isPinchToZoomActive = false
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
} : nil
)),
environment: {},
containerSize: itemFrame.size
@ -1158,7 +1195,7 @@ final class VideoChatParticipantsComponent: Component {
if participant.peer.id == component.call.accountContext.account.peerId {
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
} else if component.speakingParticipants.contains(participant.peer.id) {
if let volume = participant.volume, volume != 10000 {
if let volume = participant.volume, volume / 100 != 100 {
subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive)
} else {
subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive)
@ -1322,17 +1359,8 @@ final class VideoChatParticipantsComponent: Component {
))
}*/
let expandedControlsAlpha: CGFloat = expandedVideoState.isUIHidden ? 0.0 : 1.0
let expandedControlsAlpha: CGFloat = (expandedVideoState.isUIHidden || self.isPinchToZoomActive) ? 0.0 : 1.0
let expandedThumbnailsAlpha: CGFloat = expandedControlsAlpha
/*if itemLayout.layout.videoColumn == nil {
if expandedVideoState.isUIHidden {
expandedThumbnailsAlpha = 0.0
} else {
expandedThumbnailsAlpha = 1.0
}
} else {
expandedThumbnailsAlpha = 0.0
}*/
var expandedThumbnailsTransition = transition
let expandedThumbnailsView: ComponentView<Empty>
@ -1359,12 +1387,21 @@ final class VideoChatParticipantsComponent: Component {
return
}
component.updateMainParticipant(VideoParticipantKey(id: key.id, isPresentation: key.isPresentation), nil)
},
contextAction: { [weak self] peer, sourceView, gesture in
guard let self, let component = self.component else {
return
}
component.openParticipantContextMenu(peer.id, sourceView, gesture)
}
)),
environment: {},
containerSize: itemLayout.expandedGrid.itemContainerFrame().size
)
let expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize)
if expandedVideoState.isUIHidden {
expandedThumbnailsFrame.origin.y += expandedThumbnailsSize.height
}
if let expandedThumbnailsComponentView = expandedThumbnailsView.view {
if expandedThumbnailsComponentView.superview == nil {
self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView)
@ -1577,9 +1614,33 @@ final class VideoChatParticipantsComponent: Component {
self.isUpdating = false
}
let previousComponent = self.component
self.component = component
self.state = state
if let expandedVideoState = component.expandedVideoState, expandedVideoState.isUIHidden {
if self.stopRequestingNonCentralVideoTimer == nil || previousComponent?.expandedVideoState != expandedVideoState {
self.stopRequestingNonCentralVideoTimer?.invalidate()
self.stopRequestingNonCentralVideoTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.stopRequestingNonCentralVideo = true
self.stopRequestingNonCentralVideoTimer = nil
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
})
}
} else {
self.stopRequestingNonCentralVideo = false
if let stopRequestingNonCentralVideoTimer = self.stopRequestingNonCentralVideoTimer {
self.stopRequestingNonCentralVideoTimer = nil
stopRequestingNonCentralVideoTimer.invalidate()
}
}
let measureListItemSize = self.measureListItemView.update(
transition: .immediate,
component: AnyComponent(PeerListItemComponent(
@ -1727,13 +1788,19 @@ final class VideoChatParticipantsComponent: Component {
}
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxVideoQuality != .full {
} else {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
}
}
if let videoChannel = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: maxPresentationQuality) {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
if self.stopRequestingNonCentralVideo && component.expandedVideoState != nil && maxPresentationQuality != .full {
} else {
if !requestedVideo.contains(videoChannel) {
requestedVideo.append(videoChannel)
}
}
}
}

View file

@ -22,6 +22,7 @@ import ShareController
import AvatarNode
import TelegramAudio
import LegacyComponents
import TooltipUI
final class VideoChatScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -83,6 +84,7 @@ final class VideoChatScreenComponent: Component {
var scheduleInfo: ComponentView<Empty>?
var reconnectedAsEventsDisposable: Disposable?
var memberEventsDisposable: Disposable?
var peer: EnginePeer?
var callState: PresentationGroupCallState?
@ -144,6 +146,7 @@ final class VideoChatScreenComponent: Component {
self.membersDisposable?.dispose()
self.applicationStateDisposable?.dispose()
self.reconnectedAsEventsDisposable?.dispose()
self.memberEventsDisposable?.dispose()
self.displayAsPeersDisposable?.dispose()
self.audioOutputStateDisposable?.dispose()
self.inviteLinksDisposable?.dispose()
@ -819,7 +822,7 @@ final class VideoChatScreenComponent: Component {
self.members = members
if let members, let _ = self.expandedParticipantsVideoState {
if let members, let expandedParticipantsVideoState = self.expandedParticipantsVideoState, !expandedParticipantsVideoState.isUIHidden {
var videoCount = 0
for participant in members.participants {
if participant.presentationDescription != nil {
@ -1008,6 +1011,31 @@ final class VideoChatScreenComponent: Component {
}
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
})
self.memberEventsDisposable = (component.call.memberEvents
|> deliverOnMainQueue).start(next: { [weak self] event in
guard let self, let members = self.members, let component = self.component, let environment = self.environment else {
return
}
if event.joined {
var displayEvent = false
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
displayEvent = false
}
if members.totalCount < 250 {
displayEvent = true
} else if event.peer.isVerified {
displayEvent = true
} else if event.isContact || event.isInChatList {
displayEvent = true
}
if displayEvent {
let text = environment.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
}
})
}
self.isPresentedValue.set(environment.isVisible)
@ -1072,7 +1100,7 @@ final class VideoChatScreenComponent: Component {
} else {
containerOffset = verticalPanState.fraction * availableSize.height
}
self.containerView.layer.cornerRadius = environment.deviceMetrics.screenCornerRadius
self.containerView.layer.cornerRadius = containerOffset.isZero ? 0.0 : environment.deviceMetrics.screenCornerRadius
}
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerOffset), size: availableSize), completion: { [weak self] completed in
@ -1249,21 +1277,61 @@ final class VideoChatScreenComponent: Component {
} else {
idleTitleStatusText = " "
}
let canManageCall = self.callState?.canManageCall ?? false
var maxTitleWidth: CGFloat = availableSize.width - sideInset * 2.0 - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0
if isTwoColumnLayout {
maxTitleWidth -= 110.0
}
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(VideoChatTitleComponent(
title: self.callState?.title ?? self.peer?.debugDisplayTitle ?? " ",
status: idleTitleStatusText,
isRecording: self.callState?.recordingStartTimestamp != nil,
strings: environment.strings
strings: environment.strings,
tapAction: self.callState?.recordingStartTimestamp != nil ? { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
return
}
guard let titleView = self.title.view as? VideoChatTitleComponent.View, let recordingIndicatorView = titleView.recordingIndicatorView else {
return
}
var hasTooltipAlready = false
environment.controller()?.forEachController { controller -> Bool in
if controller is TooltipScreen {
hasTooltipAlready = true
}
return true
}
if !hasTooltipAlready {
let location = recordingIndicatorView.convert(recordingIndicatorView.bounds, to: self)
let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
text = environment.strings.LiveStream_RecordingInProgress
} else {
text = environment.strings.VoiceChat_RecordingInProgress
}
environment.controller()?.present(TooltipScreen(account: component.call.accountContext.account, sharedContext: component.call.accountContext.sharedContext, text: .plain(text: text), icon: nil, location: .point(location.offsetBy(dx: 1.0, dy: 0.0), .top), displayDuration: .custom(3.0), shouldDismissOnTouch: { _, _ in
return .dismiss(consume: true)
}), in: .current)
}
} : nil,
longTapAction: canManageCall ? { [weak self] in
guard let self else {
return
}
self.openTitleEditing()
} : nil
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - navigationButtonAreaWidth * 2.0 - 4.0 * 2.0, height: 100.0)
containerSize: CGSize(width: maxTitleWidth, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: topInset + floor((navigationBarHeight - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerView.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
@ -1436,7 +1504,7 @@ final class VideoChatScreenComponent: Component {
component: AnyComponent(VideoChatParticipantsComponent(
call: component.call,
participants: mappedParticipants,
speakingParticipants: members?.speakingParticipants ?? Set(),
speakingParticipants: self.members?.speakingParticipants ?? Set(),
expandedVideoState: self.expandedParticipantsVideoState,
theme: environment.theme,
strings: environment.strings,
@ -1699,7 +1767,9 @@ final class VideoChatScreenComponent: Component {
let videoButtonContent: VideoChatActionButtonComponent.Content
if let callState = self.callState, let muteState = callState.muteState, !muteState.canUnmute {
var buttonAudio: VideoChatActionButtonComponent.Content.Audio = .speaker
var buttonIsEnabled = false
if let (availableOutputs, maybeCurrentOutput) = self.audioOutputState, let currentOutput = maybeCurrentOutput {
buttonIsEnabled = availableOutputs.count > 1
switch currentOutput {
case .builtin:
buttonAudio = .builtin
@ -1723,7 +1793,7 @@ final class VideoChatScreenComponent: Component {
buttonAudio = .none
}
}
videoButtonContent = .audio(audio: buttonAudio)
videoButtonContent = .audio(audio: buttonAudio, isEnabled: buttonIsEnabled)
} else {
//TODO:release
videoButtonContent = .video(isActive: false)

View file

@ -12,17 +12,23 @@ final class VideoChatTitleComponent: Component {
let status: String
let isRecording: Bool
let strings: PresentationStrings
let tapAction: (() -> Void)?
let longTapAction: (() -> Void)?
init(
title: String,
status: String,
isRecording: Bool,
strings: PresentationStrings
strings: PresentationStrings,
tapAction: (() -> Void)?,
longTapAction: (() -> Void)?
) {
self.title = title
self.status = status
self.isRecording = isRecording
self.strings = strings
self.tapAction = tapAction
self.longTapAction = longTapAction
}
static func ==(lhs: VideoChatTitleComponent, rhs: VideoChatTitleComponent) -> Bool {
@ -38,6 +44,12 @@ final class VideoChatTitleComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if (lhs.tapAction == nil) != (rhs.tapAction == nil) {
return false
}
if (lhs.longTapAction == nil) != (rhs.longTapAction == nil) {
return false
}
return true
}
@ -55,6 +67,12 @@ final class VideoChatTitleComponent: Component {
private var currentActivityStatus: String?
private var currentSize: CGSize?
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
public var recordingIndicatorView: UIView? {
return self.recordingImageView
}
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
@ -67,12 +85,33 @@ final class VideoChatTitleComponent: Component {
}
self.updateAnimations()
}
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
tapRecognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.addGestureRecognizer(tapRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
guard let component = self.component else {
return
}
if case .ended = recognizer.state {
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
if case .tap = gesture {
component.tapAction?()
} else if case .longTap = gesture {
component.longTapAction?()
}
}
}
}
private func updateAnimations() {
if let recordingImageView = self.recordingImageView {
if recordingImageView.layer.animation(forKey: "blink") == nil {
@ -153,15 +192,22 @@ final class VideoChatTitleComponent: Component {
self.component = component
self.tapRecognizer?.isEnabled = component.longTapAction != nil || component.tapAction != nil
let spacing: CGFloat = 1.0
var maxTitleWidth = availableSize.width
if component.isRecording {
maxTitleWidth -= 10.0
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
containerSize: CGSize(width: maxTitleWidth, height: 100.0)
)
let statusComponent: AnyComponent<Empty>
@ -181,15 +227,18 @@ final class VideoChatTitleComponent: Component {
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: 0.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
titleView.isUserInteractionEnabled = false
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.center)
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
let statusFrame = CGRect(origin: CGPoint(x: floor((size.width - statusSize.width) * 0.5), y: titleFrame.maxY + spacing), size: statusSize)
if let statusView = self.status.view {
if statusView.superview == nil {
statusView.isUserInteractionEnabled = false
self.addSubview(statusView)
}
transition.setPosition(view: statusView, position: statusFrame.center)

View file

@ -5,12 +5,16 @@ import Display
private final class VoiceChatMicrophoneNodeDrawingState: NSObject {
let color: UIColor
let shadowColor: UIColor?
let shadowBlur: CGFloat
let filled: Bool
let transition: CGFloat
let reverse: Bool
init(color: UIColor, filled: Bool, transition: CGFloat, reverse: Bool) {
init(color: UIColor, shadowColor: UIColor?, shadowBlur: CGFloat, filled: Bool, transition: CGFloat, reverse: Bool) {
self.color = color
self.shadowColor = shadowColor
self.shadowBlur = shadowBlur
self.filled = filled
self.transition = transition
self.reverse = reverse
@ -24,11 +28,15 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
let muted: Bool
let color: UIColor
let filled: Bool
let shadowColor: UIColor?
let shadowBlur: CGFloat
init(muted: Bool, filled: Bool, color: UIColor) {
init(muted: Bool, filled: Bool, color: UIColor, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0) {
self.muted = muted
self.filled = filled
self.color = color
self.shadowColor = shadowColor
self.shadowBlur = shadowBlur
}
static func ==(lhs: State, rhs: State) -> Bool {
@ -41,6 +49,12 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
if lhs.filled != rhs.filled {
return false
}
if lhs.shadowColor != rhs.shadowColor {
return false
}
if lhs.shadowBlur != rhs.shadowBlur {
return false
}
return true
}
}
@ -122,6 +136,8 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
var transitionFraction: CGFloat = self.state.muted ? 1.0 : 0.0
var color = self.state.color
var shadowColor = self.state.shadowColor
var shadowBlur = self.state.shadowBlur
var reverse = false
if let transitionContext = self.transitionContext {
@ -138,9 +154,17 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
if transitionContext.previousState.color.rgb != color.rgb {
color = transitionContext.previousState.color.interpolateTo(color, fraction: t)!
}
if let previousShadowColor = transitionContext.previousState.shadowColor, let shadowColorValue = shadowColor, previousShadowColor.rgb != shadowColorValue.rgb {
shadowColor = previousShadowColor.interpolateTo(shadowColorValue, fraction: t)!
}
if transitionContext.previousState.shadowBlur != shadowBlur {
shadowBlur = transitionContext.previousState.shadowBlur * (1.0 - t) + shadowBlur * t
}
}
return VoiceChatMicrophoneNodeDrawingState(color: color, filled: self.state.filled, transition: transitionFraction, reverse: reverse)
return VoiceChatMicrophoneNodeDrawingState(color: color, shadowColor: shadowColor, shadowBlur: shadowBlur, filled: self.state.filled, transition: transitionFraction, reverse: reverse)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
@ -155,9 +179,18 @@ final class VoiceChatMicrophoneNode: ASDisplayNode {
guard let parameters = parameters as? VoiceChatMicrophoneNodeDrawingState else {
return
}
var bounds = bounds
bounds = bounds.insetBy(dx: parameters.shadowBlur, dy: parameters.shadowBlur)
context.translateBy(x: bounds.minX, y: bounds.minY)
context.setFillColor(parameters.color.cgColor)
if let shadowColor = parameters.shadowColor, parameters.shadowBlur != 0.0 {
context.setShadow(offset: CGSize(), blur: parameters.shadowBlur, color: shadowColor.cgColor)
}
var clearLineWidth: CGFloat = 2.0
var lineWidth: CGFloat = 1.0 + UIScreenPixel
if bounds.size.width > 36.0 {

View file

@ -254,7 +254,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
}
case let .messageActionRequestedPeer(_, peers):
result.append(contentsOf: peers.map(\.peerId))
case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _):
case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _, _):
if let boostPeer = boostPeer {
result.append(boostPeer.peerId)
}

View file

@ -100,8 +100,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
return TelegramMediaAction(action: .joinedByRequest)
case let .messageActionWebViewDataSentMe(text, _), let .messageActionWebViewDataSent(text):
return TelegramMediaAction(action: .webViewData(text))
case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount):
return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount))
case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount, message):
let text: String?
let entities: [MessageTextEntity]?
switch message {
case let .textWithEntities(textValue, entitiesValue):
text = textValue
entities = messageTextEntitiesFromApiEntities(entitiesValue)
default:
text = nil
entities = nil
}
return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities))
case let .messageActionGiftStars(_, currency, amount, stars, cryptoCurrency, cryptoAmount, transactionId):
return TelegramMediaAction(action: .giftStars(currency: currency, amount: amount, count: stars, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId))
case let .messageActionTopicCreate(_, title, iconColor, iconEmojiId):
@ -133,8 +143,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
} else {
return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper), forBoth: (flags & (1 << 1)) != 0))
}
case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount):
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount))
case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount, message):
let text: String?
let entities: [MessageTextEntity]?
switch message {
case let .textWithEntities(textValue, entitiesValue):
text = textValue
entities = messageTextEntitiesFromApiEntities(entitiesValue)
default:
text = nil
entities = nil
}
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities))
case let .messageActionGiveawayLaunch(_, stars):
return TelegramMediaAction(action: .giveawayLaunched(stars: stars))
case let .messageActionGiveawayResults(flags, winners, unclaimed):

View file

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 189
return 190
}
public func parseMessage(_ data: Data!) -> Any! {

View file

@ -114,7 +114,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case setChatTheme(emoji: String)
case joinedByRequest
case webViewData(String)
case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?)
case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?)
case topicCreated(title: String, iconColor: Int32, iconFileId: Int64?)
case topicEdited(components: [ForumTopicEditComponent])
case suggestedProfilePhoto(image: TelegramMediaImage?)
@ -122,7 +122,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case requestedPeer(buttonId: Int32, peerIds: [PeerId])
case setChatWallpaper(wallpaper: TelegramWallpaper, forBoth: Bool)
case setSameChatWallpaper(wallpaper: TelegramWallpaper)
case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?)
case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?)
case giveawayLaunched(stars: Int64?)
case joinedChannel
case giveawayResults(winners: Int32, unclaimed: Int32, stars: Bool)
@ -200,7 +200,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 26:
self = .webViewData(decoder.decodeStringForKey("t", orElse: ""))
case 27:
self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"))
self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"))
case 28:
self = .topicCreated(title: decoder.decodeStringForKey("title", orElse: ""), iconColor: decoder.decodeInt32ForKey("iconColor", orElse: 0), iconFileId: decoder.decodeOptionalInt64ForKey("iconFileId"))
case 29:
@ -230,7 +230,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 35:
self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) })
case 36:
self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"))
self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"))
case 37:
self = .giveawayLaunched(stars: decoder.decodeOptionalInt64ForKey("stars"))
case 38:
@ -382,7 +382,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case let .webViewData(text):
encoder.encodeInt32(26, forKey: "_rawValue")
encoder.encodeString(text, forKey: "t")
case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount):
case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount, text, entities):
encoder.encodeInt32(27, forKey: "_rawValue")
encoder.encodeString(currency, forKey: "currency")
encoder.encodeInt64(amount, forKey: "amount")
@ -390,6 +390,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
if let cryptoCurrency = cryptoCurrency, let cryptoAmount = cryptoAmount {
encoder.encodeString(cryptoCurrency, forKey: "cryptoCurrency")
encoder.encodeInt64(cryptoAmount, forKey: "cryptoAmount")
} else {
encoder.encodeNil(forKey: "cryptoCurrency")
encoder.encodeNil(forKey: "cryptoAmount")
}
if let text, let entities {
encoder.encodeString(text, forKey: "text")
encoder.encodeObjectArray(entities, forKey: "entities")
} else {
encoder.encodeNil(forKey: "text")
encoder.encodeNil(forKey: "entities")
}
case let .topicCreated(title, iconColor, iconFileId):
encoder.encodeInt32(28, forKey: "_rawValue")
@ -433,7 +443,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "atp")
}
case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount):
case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount, text, entities):
encoder.encodeInt32(36, forKey: "_rawValue")
encoder.encodeString(slug, forKey: "slug")
encoder.encodeBool(fromGiveaway, forKey: "give")
@ -464,6 +474,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "cryptoAmount")
}
if let text, let entities {
encoder.encodeString(text, forKey: "text")
encoder.encodeObjectArray(entities, forKey: "entities")
} else {
encoder.encodeNil(forKey: "text")
encoder.encodeNil(forKey: "entities")
}
case let .giveawayLaunched(stars):
encoder.encodeInt32(37, forKey: "_rawValue")
if let stars = stars {
@ -563,7 +580,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
return peerIds
case let .requestedPeer(_, peerIds):
return peerIds
case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _):
case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _):
return boostPeerId.flatMap { [$0] } ?? []
case let .paymentRefunded(peerId, _, _, _, _):
return [peerId]

View file

@ -15,7 +15,7 @@ public enum AppStoreTransactionPurpose {
case upgrade
case restore
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64)
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64, text: String?, entities: [MessageTextEntity]?)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
case stars(count: Int64, currency: String, amount: Int64)
case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64)
@ -43,7 +43,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
}
return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount))
}
case let .giftCode(peerIds, boostPeerId, currency, amount):
case let .giftCode(peerIds, boostPeerId, currency, amount, text, entities):
return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in
var flags: Int32 = 0
var apiBoostPeer: Api.InputPeer?
@ -59,8 +59,13 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
apiBoostPeer = apiPeer
flags |= (1 << 0)
}
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount)
var message: Api.TextWithEntities?
if let text {
flags |= (1 << 1)
message = .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities ?? [], associatedPeers: SimpleDictionary()))
}
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount, message: message)
}
case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount):
return account.postbox.transaction { transaction -> Signal<Api.InputStorePaymentPurpose, NoError> in

View file

@ -293,7 +293,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv
}
}
let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount)
let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: nil)
var flags: Int32 = 0
if let _ = option.storeProductId {

View file

@ -462,6 +462,10 @@ public extension EnginePeer {
var addressName: String? {
return self._asPeer().addressName
}
var usernames: [TelegramPeerUsername] {
return self._asPeer().usernames
}
var indexName: EnginePeer.IndexName {
return EnginePeer.IndexName(self._asPeer().indexName)

View file

@ -51,7 +51,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
}
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var renderedMyPeers: [FoundPeer] = []
@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
if let group = peer as? TelegramGroup, group.migrationReference != nil {
continue
}
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
if let user = peer as? TelegramUser {
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
} else {
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
}
}
}
@ -72,7 +76,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
if let group = peer as? TelegramGroup, group.migrationReference != nil {
continue
}
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
if let user = peer as? TelegramUser {
renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
} else {
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
}
}
}

View file

@ -735,7 +735,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
}
case let .webViewData(text):
attributedString = NSAttributedString(string: strings.Notification_WebAppSentData(text).string, font: titleFont, textColor: primaryTextColor)
case let .giftPremium(currency, amount, _, _, _):
case let .giftPremium(currency, amount, _, _, _, _, _):
let price = formatCurrencyAmount(amount, currency: currency)
if message.author?.id == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
@ -952,7 +952,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
}
case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _):
case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _, _, _):
if boostPeerId == nil, let currency, let amount {
let price = formatCurrencyAmount(amount, currency: currency)
if message.author?.id == accountPeerId {
@ -1058,7 +1058,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
let _ = text
let _ = entities
let starsPrice = "\(gift.price) Stars"
let starsPrice = strings.Notification_StarsGift_Stars(Int32(gift.price))
var authorName = compactAuthorName
var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)]
if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 {

View file

@ -511,7 +511,7 @@ private final class VideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init() {
self.contentContainerNode = ASDisplayNode()
@ -531,9 +531,9 @@ private final class VideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -591,8 +591,8 @@ private final class VideoDecoration: UniversalVideoDecoration {
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
@ -607,7 +607,7 @@ private final class VideoDecoration: UniversalVideoDecoration {
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}

View file

@ -343,15 +343,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
if oldValue != self.visibility {
switch self.visibility {
case .none:
self.textNode.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
self.textNode.visibilityRect = subRect
}
self.updateVisibility()
}
}
}
@ -594,6 +586,21 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations
var effectiveVisibility = self.visibility
if !isPlaying {
effectiveVisibility = .none
}
switch effectiveVisibility {
case .none:
self.textNode.visibilityRect = nil
case let .visible(_, subRect):
var subRect = subRect
subRect.origin.x = 0.0
subRect.size.width = 10000.0
self.textNode.visibilityRect = subRect
}
var canPlayEffects = isPlaying
if !item.controllerInteraction.canReadHistory {
canPlayEffects = false

View file

@ -680,22 +680,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
override public var visibility: ListViewItemNodeVisibility {
didSet {
if self.visibility != oldValue {
for contentNode in self.contentNodes {
contentNode.visibility = mapVisibility(self.visibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
}
if let threadInfoNode = self.threadInfoNode {
threadInfoNode.visibility = self.visibility != .none
}
if let replyInfoNode = self.replyInfoNode {
replyInfoNode.visibility = self.visibility != .none
}
if let unlockButtonNode = self.unlockButtonNode {
unlockButtonNode.visibility = self.visibility != .none
}
self.visibilityStatus = self.visibility != .none
self.updateVisibility()
@ -718,6 +702,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
private var forceStopAnimations: Bool = false
required public init(rotated: Bool) {
self.mainContextSourceNode = ContextExtractedContentContainingNode()
self.mainContainerNode = ContextControllerSourceNode()
@ -6207,6 +6193,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return false
}
override public func updateStickerSettings(forceStopAnimations: Bool) {
self.forceStopAnimations = forceStopAnimations
self.updateVisibility()
}
private func updateVisibility() {
guard let item = self.item else {
return
@ -6223,11 +6214,35 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if !item.controllerInteraction.canReadHistory {
isPlaying = false
}
if self.forceStopAnimations {
isPlaying = false
}
if !isPlaying {
self.removeEffectAnimations()
}
var effectiveVisibility = self.visibility
if !isPlaying {
effectiveVisibility = .none
}
for contentNode in self.contentNodes {
contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
}
if let threadInfoNode = self.threadInfoNode {
threadInfoNode.visibility = effectiveVisibility != .none
}
if let replyInfoNode = self.replyInfoNode {
replyInfoNode.visibility = effectiveVisibility != .none
}
if let unlockButtonNode = self.unlockButtonNode {
unlockButtonNode.visibility = effectiveVisibility != .none
}
if isPlaying {
var alreadySeen = true
if item.message.flags.contains(.Incoming) {

View file

@ -37,6 +37,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let titleNode: TextNode
private let subtitleNode: TextNodeWithEntities
private let textClippingNode: ASDisplayNode
private var dustNode: InvisibleInkDustNode?
private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode
@ -49,11 +50,17 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private let buttonStarsNode: PremiumStarsNode
private let buttonTitleNode: TextNode
private var maskView: UIImageView?
private var maskOverlayView: UIView?
private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])?
private var absoluteRect: (CGRect, CGSize)?
private var isPlaying: Bool = false
private var isExpanded: Bool = false
private var appliedIsExpanded: Bool = false
private var currentProgressDisposable: Disposable?
override public var visibility: ListViewItemNodeVisibility {
@ -105,6 +112,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.subtitleNode.textNode.isUserInteractionEnabled = false
self.subtitleNode.textNode.displaysAsynchronously = false
self.textClippingNode = ASDisplayNode()
self.textClippingNode.clipsToBounds = true
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0
@ -133,7 +143,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.labelNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode.textNode)
self.addSubnode(self.textClippingNode)
self.textClippingNode.addSubnode(self.subtitleNode.textNode)
self.addSubnode(self.placeholderNode)
self.addSubnode(self.animationNode)
@ -251,9 +262,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
let makeMeasureTextLayout = TextNode.asyncLayout(nil)
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
let currentIsExpanded = self.isExpanded
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
@ -279,9 +293,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
for media in item.message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
case let .giftPremium(_, _, monthsValue, _, _):
case let .giftPremium(_, _, monthsValue, _, _, giftText, giftEntities):
months = monthsValue
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
if months == 12 {
title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1)
} else {
title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months)
}
if let giftText, !giftText.isEmpty {
text = giftText
entities = giftEntities ?? []
} else {
text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription
}
case let .giftStars(_, _, count, _, _, _):
if count <= 1000 {
months = 3
@ -310,10 +334,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
title = item.presentationData.strings.Notification_StarsGiveaway_Title
text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(count))).string
case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _):
case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities):
if channelId == nil {
months = monthsValue
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
if months == 12 {
title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1)
} else {
title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months)
}
if let giftText, !giftText.isEmpty {
text = giftText
entities = giftEntities ?? []
} else {
text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription
}
if item.message.author?.id != item.context.account.peerId {
buttonTitle = item.presentationData.strings.Notification_PremiumGift_UseGift
}
@ -419,13 +453,24 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
), textAlignment: .center)
}
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var canExpand = false
var clippedTextHeight: CGFloat = subtitleLayout.size.height
if subtitleLayout.numberOfLines > 4 {
let (measuredTextLayout, _) = makeMeasureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
canExpand = true
if !currentIsExpanded {
clippedTextHeight = measuredTextLayout.size.height
}
}
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 164.0
giftSize.height = titleLayout.size.height + textSpacing + clippedTextHeight + 164.0
if !buttonTitle.isEmpty {
giftSize.height += 48.0
}
@ -472,8 +517,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
return (backgroundSize, { [weak self] animation, synchronousLoads, info in
if let strongSelf = self {
let isFirstTime = strongSelf.item == nil
var isExpandedUpdated = false
if strongSelf.appliedIsExpanded != currentIsExpanded {
strongSelf.appliedIsExpanded = currentIsExpanded
info?.setInvertOffsetDirection()
isExpandedUpdated = true
}
let _ = isExpandedUpdated
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize)
@ -551,8 +606,21 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: boundingWidth, height: clippedTextHeight))
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size)
strongSelf.subtitleNode.textNode.frame = subtitleFrame
strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size)
if isFirstTime {
strongSelf.textClippingNode.frame = clippingTextFrame
} else {
animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil)
}
if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView {
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
}
if !subtitleLayout.spoilers.isEmpty {
let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
@ -573,11 +641,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.dustNode = nil
}
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size)
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: clippingTextFrame.maxY + 18.0), size: buttonTitleLayout.size)
strongSelf.buttonTitleNode.frame = buttonTitleFrame
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize)
strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize)
if ribbonTextLayout.size.width > 0.0 {
@ -666,6 +734,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.updateAbsoluteRect(rect, within: size)
}
if canExpand {
if strongSelf.maskView?.image == nil {
strongSelf.maskView?.image = generateMaskImage()
}
strongSelf.textClippingNode.view.mask = strongSelf.maskView
// var expandIconFrame: CGRect = .zero
// if let icon = strongSelf.expandIcon.image {
// expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size)
// if wasHidden || isFirstTime {
// strongSelf.expandIcon.position = expandIconFrame.center
// } else {
// animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil)
// }
// strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size)
// }
} else {
strongSelf.textClippingNode.view.mask = nil
}
switch strongSelf.visibility {
case .none:
strongSelf.subtitleNode.visibilityRect = nil
@ -853,3 +941,22 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
}
}
private func generateMaskImage() -> UIImage? {
return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: size))
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.setBlendMode(.copy)
context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0)))
context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0))
}

View file

@ -1425,6 +1425,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
if let strongSelf = self {
if file.isAnimated {
strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: file), reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).startStrict())
} else if NativeVideoContent.isHLSVideo(file: file) {
strongSelf.fetchDisposable.set(nil)
} else {
strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual, storeToDownloadsPeerId: storeToDownloadsPeerId).startStrict())
}
@ -1659,16 +1661,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
let loopVideo = updatedVideoFile.isAnimated
let videoContent: UniversalVideoContent
if !"".isEmpty && NativeVideoContent.isHLSVideo(file: updatedVideoFile) {
videoContent = HLSVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: true, loopVideo: loopVideo)
} else {
videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in
guard let context, let peerId else {
return
}
let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone()
})
}
videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor, captureProtected: message.isCopyProtected() || isExtendedMedia, storeAfterDownload: { [weak context] in
guard let context, let peerId else {
return
}
let _ = storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: .message(message: MessageReference(message), media: updatedVideoFile), peerId: peerId).startStandalone()
})
let videoNode = UniversalVideoNode(accountId: context.account.id, postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.isUserInteractionEnabled = false
videoNode.ownsContentNodeUpdated = { [weak self] owns in
@ -1850,7 +1848,32 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
if case .full = automaticDownload {
if automaticDownload != .none, let file = media as? TelegramMediaFile, NativeVideoContent.isHLSVideo(file: file) {
let postbox = context.account.postbox
let fetchSignal = HLSVideoContent.minimizedHLSQualityPreloadData(postbox: context.account.postbox, file: .message(message: MessageReference(message), media: file), userLocation: .peer(message.id.peerId), prefixSeconds: 10, autofetchPlaylist: true)
|> mapToSignal { fileAndRange -> Signal<Never, NoError> in
guard let fileAndRange else {
return .complete()
}
return freeMediaFileResourceInteractiveFetched(postbox: postbox, userLocation: .peer(message.id.peerId), fileReference: fileAndRange.0, resource: fileAndRange.0.media.resource, range: (fileAndRange.1, .default))
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
}
let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get()
|> mapToSignal { visibility -> Signal<Void, NoError> in
if visibility {
return fetchSignal
|> mapToSignal { _ -> Signal<Void, NoError> in
}
} else {
return .complete()
}
}
strongSelf.fetchDisposable.set(visibilityAwareFetchSignal.startStrict())
} else if case .full = automaticDownload {
if let _ = media as? TelegramMediaImage {
updatedFetchControls.fetch(false)
} else if let image = media as? TelegramMediaWebFile {

View file

@ -24,6 +24,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode",
"//submodules/TelegramUniversalVideoContent",
],
visibility = [
"//visibility:public",

View file

@ -16,6 +16,7 @@ import ChatMessageItemCommon
import ChatMessageInteractiveMediaNode
import ChatControllerInteraction
import InvisibleInkDustNode
import TelegramUniversalVideoContent
public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
override public var supportsMosaic: Bool {
@ -163,7 +164,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo {
if case .full = automaticDownload {
if NativeVideoContent.isHLSVideo(file: telegramFile) {
automaticPlayback = true
} else if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
@ -207,7 +210,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo {
if case .full = automaticDownload {
if NativeVideoContent.isHLSVideo(file: telegramFile) {
automaticPlayback = true
} else if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil

View file

@ -101,6 +101,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
private var forceStopAnimations: Bool = false
required public init(rotated: Bool) {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
@ -2160,6 +2162,9 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
if !item.controllerInteraction.canReadHistory {
isPlaying = false
}
if self.forceStopAnimations {
isPlaying = false
}
if !isPlaying {
self.removeEffectAnimations()
@ -2191,6 +2196,11 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
override public func updateStickerSettings(forceStopAnimations: Bool) {
self.forceStopAnimations = forceStopAnimations
self.updateVisibility()
}
override public func messageEffectTargetView() -> UIView? {
if let result = self.dateAndStatusNode.messageEffectTargetView() {
return result

View file

@ -468,8 +468,12 @@ private final class SheetContent: CombinedComponent {
switch component.subject {
case .peer:
if let peer = state.peer {
if case .user = peer {
mainTitle = environment.strings.Report_Title_User
if case let .user(user) = peer {
if let _ = user.botInfo {
mainTitle = environment.strings.Report_Title_Bot
} else {
mainTitle = environment.strings.Report_Title_User
}
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
mainTitle = environment.strings.Report_Title_Channel
} else {

View file

@ -306,7 +306,7 @@ final class GiftOptionsScreenComponent: Component {
let giftController = GiftSetupScreen(
context: component.context,
peerId: component.peerId,
gift: gift,
subject: .starGift(gift),
completion: component.completion
)
mainController.push(giftController)
@ -359,94 +359,6 @@ final class GiftOptionsScreenComponent: Component {
}
}
private func buyPremium(_ product: PremiumGiftProduct) {
guard let component = self.component, let inAppPurchaseManager = self.component?.context.inAppPurchaseManager, self.inProgressPremiumGift == nil else {
return
}
self.inProgressPremiumGift = product.id
self.state?.updated()
let (currency, amount) = product.storeProduct.priceCurrencyAndAmount
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept")
let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount)
let quantity: Int32 = 1
let completion = component.completion
let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
if let strongSelf = self {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if available {
strongSelf.purchaseDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, quantity: quantity, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let completion {
completion()
} else {
guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftOptionsScreen) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}
}, error: { [weak self] error in
guard let self, let controller = self.environment?.controller() else {
return
}
self.inProgressPremiumGift = nil
self.state?.updated(transition: .immediate)
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .tryLater:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
if let errorText {
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail")
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller.present(alertController, in: .window(.root))
}
}))
} else {
self?.inProgressPremiumGift = nil
self?.state?.updated(transition: .immediate)
}
}
})
}
func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
@ -707,7 +619,7 @@ final class GiftOptionsScreenComponent: Component {
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize)
for product in premiumProducts {
let itemId = AnyHashable(product.storeProduct.id)
let itemId = AnyHashable(product.id)
validIds.append(itemId)
var itemTransition = transition
@ -756,7 +668,23 @@ final class GiftOptionsScreenComponent: Component {
),
effectAlignment: .center,
action: { [weak self] in
self?.buyPremium(product)
if let self, let component = self.component {
if let controller = controller() as? GiftOptionsScreen {
let mainController: ViewController
if let parentController = controller.parentController() {
mainController = parentController
} else {
mainController = controller
}
let giftController = GiftSetupScreen(
context: component.context,
peerId: component.peerId,
subject: .premium(product),
completion: component.completion
)
mainController.push(giftController)
}
}
},
animateAlpha: false
)
@ -1019,23 +947,43 @@ final class GiftOptionsScreenComponent: Component {
}
self.peer = peer
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) {
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
var premiumProducts: [PremiumGiftProduct] = []
for option in premiumOptions {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil))
if availableProducts.isEmpty {
var premiumProducts: [PremiumGiftProduct] = []
for option in premiumOptions {
premiumProducts.append(
PremiumGiftProduct(
giftOption: CachedPremiumGiftOption(
months: option.months,
currency: option.currency,
amount: option.amount,
botUrl: "",
storeProductId: option.storeProductId
),
storeProduct: nil,
discount: nil
)
)
}
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
} else {
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) {
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
var premiumProducts: [PremiumGiftProduct] = []
for option in premiumOptions {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil))
}
}
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
}
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
self.starGifts = starGifts
self.updated()
@ -1105,25 +1053,3 @@ open class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScree
super.containerLayoutUpdated(layout, transition: transition)
}
}
private struct PremiumGiftProduct: Equatable {
let giftOption: CachedPremiumGiftOption
let storeProduct: InAppPurchaseManager.Product
let discount: Int?
var id: String {
return self.storeProduct.id
}
var months: Int32 {
return self.giftOption.months
}
var price: String {
return self.storeProduct.price
}
var pricePerMonth: String {
return self.storeProduct.pricePerMonth(Int(self.months))
}
}

View file

@ -17,6 +17,7 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramStringFormatting",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
@ -41,6 +42,7 @@ swift_library(
"//submodules/BotPaymentsUI",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/InAppPurchaseManager",
],
visibility = [
"//visibility:public",

View file

@ -14,6 +14,10 @@ import WallpaperBackgroundNode
import ListItemComponentAdaptor
final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
enum Subject: Equatable {
case premium(months: Int32, amount: Int64, currency: String)
case starGift(gift: StarGift)
}
let context: AccountContext
let theme: PresentationTheme
let componentTheme: PresentationTheme
@ -26,7 +30,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
let nameDisplayOrder: PresentationPersonNameOrder
let accountPeer: EnginePeer?
let gift: StarGift
let subject: ChatGiftPreviewItem.Subject
let text: String
let entities: [MessageTextEntity]
@ -42,7 +46,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
dateTimeFormat: PresentationDateTimeFormat,
nameDisplayOrder: PresentationPersonNameOrder,
accountPeer: EnginePeer?,
gift: StarGift,
subject: ChatGiftPreviewItem.Subject,
text: String,
entities: [MessageTextEntity]
) {
@ -57,7 +61,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.accountPeer = accountPeer
self.gift = gift
self.subject = subject
self.text = text
self.entities = entities
}
@ -206,9 +210,22 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
peers[authorPeerId] = item.accountPeer?._asPeer()
let media: [Media] = [
TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false))
]
let media: [Media]
switch item.subject {
case let .premium(months, amount, currency):
media = [
TelegramMediaAction(
action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: nil, cryptoAmount: nil, text: item.text, entities: item.entities)
)
]
case let .starGift(gift):
media = [
TelegramMediaAction(
action: .starGift(gift: gift, convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false)
)
]
}
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
}

View file

@ -7,6 +7,7 @@ import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import PresentationDataUtils
import AccountContext
import ComponentFlow
@ -27,24 +28,25 @@ import EmojiSuggestionsComponent
import ChatPresentationInterfaceState
import AudioToolbox
import TextFormat
import InAppPurchaseManager
final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let gift: StarGift
let subject: GiftSetupScreen.Subject
let completion: (() -> Void)?
init(
context: AccountContext,
peerId: EnginePeer.Id,
gift: StarGift,
subject: GiftSetupScreen.Subject,
completion: (() -> Void)? = nil
) {
self.context = context
self.peerId = peerId
self.gift = gift
self.subject = subject
self.completion = completion
}
@ -55,7 +57,7 @@ final class GiftSetupScreenComponent: Component {
if lhs.peerId != rhs.peerId {
return false
}
if lhs.gift != rhs.gift {
if lhs.subject != rhs.subject {
return false
}
return true
@ -72,6 +74,7 @@ final class GiftSetupScreenComponent: Component {
private let scrollView: ScrollView
private let navigationTitle = ComponentView<Empty>()
private let remainingCount = ComponentView<Empty>()
private let introContent = ComponentView<Empty>()
private let introSection = ComponentView<Empty>()
private let hideSection = ComponentView<Empty>()
@ -103,6 +106,7 @@ final class GiftSetupScreenComponent: Component {
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
private var hideName = false
private var inProgress = false
private var previousHadInputHeight: Bool = false
private var previousInputHeight: CGFloat?
@ -139,6 +143,8 @@ final class GiftSetupScreenComponent: Component {
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
self.disablesInteractiveKeyboardGestureRecognizer = true
}
required init?(coder: NSCoder) {
@ -189,16 +195,122 @@ final class GiftSetupScreenComponent: Component {
}
func proceed() {
guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
guard let component = self.component else {
return
}
switch component.subject {
case .premium:
self.proceedWithPremiumGift()
case .starGift:
self.proceedWithStarGift()
}
}
func proceedWithPremiumGift() {
guard let component = self.component, case let .premium(product) = component.subject, let storeProduct = product.storeProduct, let inAppPurchaseManager = component.context.inAppPurchaseManager else {
return
}
self.inProgress = true
self.state?.updated()
let (currency, amount) = storeProduct.priceCurrencyAndAmount
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept")
let entities = generateChatInputTextEntities(self.textInputState.text)
let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: self.textInputState.text.string, entities: entities)
let quantity: Int32 = 1
let completion = component.completion
let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
guard let self else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if available {
let _ = (inAppPurchaseManager.buyProduct(storeProduct, quantity: quantity, purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let completion {
completion()
} else {
guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}
}, error: { [weak self] error in
guard let self, let controller = self.environment?.controller() else {
return
}
self.state?.updated(transition: .immediate)
var errorText: String?
switch error {
case .generic:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .network:
errorText = presentationData.strings.Premium_Purchase_ErrorNetwork
case .notAllowed:
errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed
case .cantMakePayments:
errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments
case .assignFailed:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .tryLater:
errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
case .cancelled:
break
}
if let errorText {
addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail")
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller.present(alertController, in: .window(.root))
}
})
} else {
self.inProgress = false
self.state?.updated(transition: .immediate)
}
})
}
func proceedWithStarGift() {
guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else {
return
}
let proceed = { [weak self] in
guard let self else {
return
}
self.inProgress = true
self.state?.updated()
let entities = generateChatInputTextEntities(self.textInputState.text)
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: entities)
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities)
let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
@ -242,11 +354,13 @@ final class GiftSetupScreenComponent: Component {
}
navigationController.setViewControllers(controllers, animated: true)
}
starsContext.load(force: true)
})
})
}
if starsState.balance < component.gift.price {
if starsState.balance < starGift.price {
let _ = (self.optionsPromise.get()
|> filter { $0 != nil }
|> take(1)
@ -258,7 +372,7 @@ final class GiftSetupScreenComponent: Component {
context: component.context,
starsContext: starsContext,
options: options ?? [],
purpose: .starGift(peerId: component.peerId, requiredStars: component.gift.price),
purpose: .starGift(peerId: component.peerId, requiredStars: starGift.price),
completion: { [weak starsContext] stars in
starsContext?.add(balance: stars)
Queue.mainQueue().after(0.1) {
@ -349,9 +463,7 @@ final class GiftSetupScreenComponent: Component {
return
}
if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
if self.textInputState.isEditing {
textInputView.insertText(text: text)
}
textInputView.insertText(text: text)
}
},
backwardsDeleteText: { [weak self] in
@ -400,6 +512,16 @@ final class GiftSetupScreenComponent: Component {
}
}
)
if case .starGift = component.subject {
self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else {
return
}
self.options = options
})
}
}
let environment = environment[EnvironmentType.self].value
@ -453,6 +575,42 @@ final class GiftSetupScreenComponent: Component {
contentHeight += environment.navigationHeight
contentHeight += 26.0
if case let .starGift(starGift) = component.subject, let availability = starGift.availability {
let remains: Int32 = availability.remains
let position = CGFloat(remains) / CGFloat(availability.total)
let remainsString = "\(remains)"
let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator)
let remainingCountSize = self.remainingCount.update(
transition: transition,
component: AnyComponent(RemainingCountComponent(
inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3),
activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)],
inactiveTitle: environment.strings.Gift_Send_Limited,
inactiveValue: "",
inactiveTitleColor: environment.theme.list.itemSecondaryTextColor,
activeTitle: "",
activeValue: totalString,
activeTitleColor: .white,
badgeText: "\(remainsString)",
badgePosition: position,
badgeGraphPosition: position,
invertProgress: true
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let remainingCountFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight - 36.0), size: remainingCountSize)
if let remainingCountView = self.remainingCount.view {
if remainingCountView.superview == nil {
self.scrollView.addSubview(remainingCountView)
}
transition.setFrame(view: remainingCountView, frame: remainingCountFrame)
}
contentHeight += remainingCountSize.height
contentHeight -= 36.0
contentHeight += sectionSpacing
}
let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
@ -521,20 +679,30 @@ final class GiftSetupScreenComponent: Component {
inputHeight = environment.inputHeight
}
}
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
let introFooter: AnyComponent<Empty>?
switch component.subject {
case .premium:
introFooter = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_Customize_Info(peerName).string,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
))
case .starGift:
introFooter = nil
}
let introSectionSize = self.introSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_Customize_Title,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
header: nil,
footer: introFooter,
items: introSectionItems
)),
environment: {},
@ -553,6 +721,15 @@ final class GiftSetupScreenComponent: Component {
let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
if let accountPeer = self.peerMap[component.context.account.peerId] {
let subject: ChatGiftPreviewItem.Subject
switch component.subject {
case let .premium(product):
let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1)
subject = .premium(months: product.months, amount: amount, currency: currency)
case let .starGift(gift):
subject = .starGift(gift: gift)
}
let introContentSize = self.introContent.update(
transition: transition,
component: AnyComponent(
@ -569,7 +746,7 @@ final class GiftSetupScreenComponent: Component {
dateTimeFormat: environment.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
accountPeer: accountPeer,
gift: component.gift,
subject: subject,
text: self.textInputState.text.string,
entities: generateChatInputTextEntities(self.textInputState.text)
),
@ -589,55 +766,56 @@ final class GiftSetupScreenComponent: Component {
}
}
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
let hideSectionSize = self.hideSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
if case .starGift = component.subject {
let hideSectionSize = self.hideSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
maximumNumberOfLines: 0
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_HideMyName,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in
guard let self else {
return
}
self.hideName = !self.hideName
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Gift_Send_HideMyName,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in
guard let self else {
return
}
self.hideName = !self.hideName
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize)
if let hideSectionView = self.hideSection.view {
if hideSectionView.superview == nil {
self.scrollView.addSubview(hideSectionView)
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize)
if let hideSectionView = self.hideSection.view {
if hideSectionView.superview == nil {
self.scrollView.addSubview(hideSectionView)
}
transition.setFrame(view: hideSectionView, frame: hideSectionFrame)
}
transition.setFrame(view: hideSectionView, frame: hideSectionFrame)
contentHeight += hideSectionSize.height
}
contentHeight += hideSectionSize.height
contentHeight += bottomContentInset
@ -647,8 +825,18 @@ final class GiftSetupScreenComponent: Component {
if self.starImage == nil || self.starImage?.1 !== environment.theme {
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
}
let amountString = presentationStringsFormattedNumber(Int32(component.gift.price), presentationData.dateTimeFormat.groupingSeparator)
let buttonAttributedString = NSMutableAttributedString(string: "\(environment.strings.Gift_Send_Send) # \(amountString)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let buttonString: String
switch component.subject {
case let .premium(product):
let amountString = product.price
buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)"
case let .starGift(starGift):
let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator)
buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)"
}
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
@ -669,7 +857,7 @@ final class GiftSetupScreenComponent: Component {
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: false,
displaysProgress: self.inProgress,
action: { [weak self] in
self?.proceed()
}
@ -825,7 +1013,7 @@ final class GiftSetupScreenComponent: Component {
}
}
}
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) {
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode {
if self.textInputState.isEditing {
self.recenterOnTag = self.textInputTag
}
@ -1039,12 +1227,17 @@ final class GiftSetupScreenComponent: Component {
}
public final class GiftSetupScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case premium(PremiumGiftProduct)
case starGift(StarGift)
}
private let context: AccountContext
public init(
context: AccountContext,
peerId: EnginePeer.Id,
gift: StarGift,
subject: Subject,
completion: (() -> Void)? = nil
) {
self.context = context
@ -1052,7 +1245,7 @@ public final class GiftSetupScreen: ViewControllerComponentContainer {
super.init(context: context, component: GiftSetupScreenComponent(
context: context,
peerId: peerId,
gift: gift,
subject: subject,
completion: completion
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
@ -1070,9 +1263,6 @@ public final class GiftSetupScreen: ViewControllerComponentContainer {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
@ -1105,3 +1295,31 @@ private struct GiftConfiguration {
}
}
}
public struct PremiumGiftProduct: Equatable {
public let giftOption: CachedPremiumGiftOption
public let storeProduct: InAppPurchaseManager.Product?
public let discount: Int?
public var id: String {
return self.storeProduct?.id ?? (self.giftOption.storeProductId ?? "")
}
public var months: Int32 {
return self.giftOption.months
}
public var price: String {
return self.storeProduct?.price ?? formatCurrencyAmount(self.giftOption.amount, currency: self.giftOption.currency)
}
public var pricePerMonth: String {
return self.storeProduct?.pricePerMonth(Int(self.months)) ?? ""
}
public init(giftOption: CachedPremiumGiftOption, storeProduct: InAppPurchaseManager.Product?, discount: Int?) {
self.giftOption = giftOption
self.storeProduct = storeProduct
self.discount = discount
}
}

View file

@ -0,0 +1,827 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import MultilineTextComponent
import Markdown
import TextFormat
import RoundedRectWithTailPath
public class RemainingCountComponent: Component {
private let inactiveColor: UIColor
private let activeColors: [UIColor]
private let inactiveTitle: String
private let inactiveValue: String
private let inactiveTitleColor: UIColor
private let activeTitle: String
private let activeValue: String
private let activeTitleColor: UIColor
private let badgeText: String?
private let badgePosition: CGFloat
private let badgeGraphPosition: CGFloat
private let invertProgress: Bool
public init(
inactiveColor: UIColor,
activeColors: [UIColor],
inactiveTitle: String,
inactiveValue: String,
inactiveTitleColor: UIColor,
activeTitle: String,
activeValue: String,
activeTitleColor: UIColor,
badgeText: String?,
badgePosition: CGFloat,
badgeGraphPosition: CGFloat,
invertProgress: Bool = false
) {
self.inactiveColor = inactiveColor
self.activeColors = activeColors
self.inactiveTitle = inactiveTitle
self.inactiveValue = inactiveValue
self.inactiveTitleColor = inactiveTitleColor
self.activeTitle = activeTitle
self.activeValue = activeValue
self.activeTitleColor = activeTitleColor
self.badgeText = badgeText
self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress
}
public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool {
if lhs.inactiveColor != rhs.inactiveColor {
return false
}
if lhs.activeColors != rhs.activeColors {
return false
}
if lhs.inactiveTitle != rhs.inactiveTitle {
return false
}
if lhs.inactiveValue != rhs.inactiveValue {
return false
}
if lhs.inactiveTitleColor != rhs.inactiveTitleColor {
return false
}
if lhs.activeTitle != rhs.activeTitle {
return false
}
if lhs.activeValue != rhs.activeValue {
return false
}
if lhs.activeTitleColor != rhs.activeTitleColor {
return false
}
if lhs.badgeText != rhs.badgeText {
return false
}
if lhs.badgePosition != rhs.badgePosition {
return false
}
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
return false
}
if lhs.invertProgress != rhs.invertProgress {
return false
}
return true
}
public final class View: UIView {
private var component: RemainingCountComponent?
private let container: UIView
private let inactiveBackground: SimpleLayer
private let inactiveTitleLabel = ComponentView<Empty>()
private let inactiveValueLabel = ComponentView<Empty>()
private let innerLeftTitleLabel = ComponentView<Empty>()
private let innerRightTitleLabel = ComponentView<Empty>()
private let activeContainer: UIView
private let activeBackground: SimpleLayer
private let activeTitleLabel = ComponentView<Empty>()
private let activeValueLabel = ComponentView<Empty>()
private let badgeView: UIView
private let badgeMaskView: UIView
private let badgeShapeLayer = CAShapeLayer()
private let badgeForeground: SimpleLayer
private let badgeLabel: BadgeLabelView
private let badgeLabelMaskView = UIImageView()
private var badgeTailPosition: CGFloat = 0.0
private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)?
override init(frame: CGRect) {
self.container = UIView()
self.container.clipsToBounds = true
self.container.layer.cornerRadius = 9.0
self.inactiveBackground = SimpleLayer()
self.activeContainer = UIView()
self.activeContainer.clipsToBounds = true
self.activeBackground = SimpleLayer()
self.activeBackground.anchorPoint = CGPoint()
self.badgeView = UIView()
self.badgeView.alpha = 0.0
self.badgeShapeLayer.fillColor = UIColor.white.cgColor
self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.badgeMaskView = UIView()
self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer)
self.badgeView.mask = self.badgeMaskView
self.badgeForeground = SimpleLayer()
self.badgeLabel = BadgeLabelView()
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
self.badgeLabel.mask = self.badgeLabelMaskView
super.init(frame: frame)
self.addSubview(self.container)
self.container.layer.addSublayer(self.inactiveBackground)
self.container.addSubview(self.activeContainer)
self.activeContainer.layer.addSublayer(self.activeBackground)
self.addSubview(self.badgeView)
self.badgeView.layer.addSublayer(self.badgeForeground)
self.badgeView.addSubview(self.badgeLabel)
self.badgeLabelMaskView.contentMode = .scaleToFill
self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let colorsArray: [CGColor] = [
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
UIColor(rgb: 0xffffff).cgColor,
UIColor(rgb: 0xffffff).cgColor,
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
]
var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
})
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.badgeShapeAnimator?.invalidate()
}
private var didPlayAppearanceAnimation = false
func playAppearanceAnimation(component: RemainingCountComponent, badgeFullSize: CGSize, from: CGFloat? = nil) {
if from == nil {
self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
let rotationAngle: CGFloat
if badgeFullSize.width > 100.0 {
rotationAngle = 0.2
} else {
rotationAngle = 0.26
}
let to: CGFloat = self.badgeView.center.x
let positionAnimation = CABasicAnimation(keyPath: "position.x")
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? self.container.frame.width, y: 0.0))
positionAnimation.toValue = NSValue(cgPoint: CGPoint(x: to, y: 0.0))
positionAnimation.duration = 0.5
positionAnimation.fillMode = .forwards
positionAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.badgeView.layer.add(positionAnimation, forKey: "appearance1")
if from != to {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.fromValue = 0.0 as NSNumber
rotateAnimation.toValue = rotationAngle as NSNumber
rotateAnimation.duration = 0.15
rotateAnimation.fillMode = .forwards
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
rotateAnimation.isRemovedOnCompletion = false
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
Queue.mainQueue().after(0.5, {
let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
bounceAnimation.fromValue = rotationAngle as NSNumber
bounceAnimation.toValue = -0.04 as NSNumber
bounceAnimation.duration = 0.2
bounceAnimation.fillMode = .forwards
bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
bounceAnimation.isRemovedOnCompletion = false
self.badgeView.layer.add(bounceAnimation, forKey: "appearance3")
self.badgeView.layer.removeAnimation(forKey: "appearance2")
Queue.mainQueue().after(0.2) {
let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
returnAnimation.fromValue = -0.04 as NSNumber
returnAnimation.toValue = 0.0 as NSNumber
returnAnimation.duration = 0.15
returnAnimation.fillMode = .forwards
returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
self.badgeView.layer.add(returnAnimation, forKey: "appearance4")
self.badgeView.layer.removeAnimation(forKey: "appearance3")
}
})
}
if from == nil {
self.badgeView.alpha = 1.0
self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
if let badgeText = component.badgeText {
let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5)
var frameTransition = transition
if from == nil {
frameTransition = frameTransition.withAnimation(.none)
}
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition)
frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize))
}
}
var previousAvailableSize: CGSize?
func update(component: RemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
let size = CGSize(width: availableSize.width, height: 90.0)
self.badgeLabel.color = component.activeTitleColor
let lineHeight: CGFloat = 30.0
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight))
self.container.frame = containerFrame
let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition)
let activeWidth: CGFloat = containerFrame.width - activityPosition
let leftTextColor: UIColor
let rightTextColor: UIColor
if component.invertProgress {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.inactiveTitleColor
} else {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.activeTitleColor
}
if "".isEmpty {
if component.invertProgress {
let innerLeftTitleSize = self.innerLeftTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerLeftTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
}
let innerRightTitleSize = self.innerRightTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerRightTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
}
}
let inactiveTitleSize = self.inactiveTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize)
}
let inactiveValueSize = self.inactiveValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveValue,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
}
let activeTitleSize = self.activeTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeTitle,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
}
let activeValueSize = self.activeValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
if component.invertProgress {
self.container.bringSubviewToFront(self.activeContainer)
}
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize)
}
}
var progressTransition: ComponentTransition = .immediate
if !transition.animation.isImmediate {
progressTransition = .easeInOut(duration: 0.5)
}
if "".isEmpty {
if component.invertProgress {
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: size.width - activityPosition, height: lineHeight)))
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
} else {
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)))
progressTransition.setFrame(layer: self.activeBackground, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
}
if self.activeBackground.animation(forKey: "movement") == nil {
self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0)
}
}
let countWidth: CGFloat
if let badgeText = component.badgeText {
countWidth = CGFloat(badgeText.count) * 10.0
} else {
countWidth = 51.0
}
let badgeWidth: CGFloat = countWidth + 20.0
let badgeSize = CGSize(width: badgeWidth, height: 30.0)
let badgeFullSize = CGSize(width: badgeWidth, height: 30.0 + 8.0)
let tailSize = CGSize(width: 15.0, height: 6.0)
let tailRadius: CGFloat = 3.0
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize)
self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize)
self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize)
let currentBadgeX = self.badgeView.center.x
let badgePosition = component.badgePosition
if badgePosition > 1.0 - 0.15 {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0))
if progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 1.0).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 1.0)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 1.0
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: 3.0 + (size.width - 6.0) * badgePosition + 3.0, y: 56.0)
}
} else if badgePosition < 0.15 {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0))
if progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.0).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.0)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 0.0
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: (size.width - 6.0) * badgePosition, y: 56.0)
}
} else {
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
if progressTransition.animation.isImmediate {
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.5).cgPath
} else {
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.5)
self.animateBadgeTailPositionChange()
}
self.badgeTailPosition = 0.5
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
} else {
self.badgeView.center = CGPoint(x: size.width * badgePosition, y: 56.0)
}
}
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height))
if self.badgeForeground.animation(forKey: "movement") == nil {
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0)
}
self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0)
if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate {
self.didPlayAppearanceAnimation = true
if transition.animation.isImmediate {
if component.badgePosition < 0.1 {
self.badgeView.alpha = 1.0
if let badgeText = component.badgeText {
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate)
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize))
}
} else {
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize)
}
} else {
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize, from: currentBadgeX)
}
}
if self.previousAvailableSize != availableSize {
self.previousAvailableSize = availableSize
var locations: [CGFloat] = []
let delta = 1.0 / CGFloat(component.activeColors.count - 1)
for i in 0 ..< component.activeColors.count {
locations.append(delta * CGFloat(i))
}
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal)
self.badgeForeground.contentsGravity = .resizeAspectFill
self.badgeForeground.contents = gradient?.cgImage
self.activeBackground.contentsGravity = .resizeAspectFill
self.activeBackground.contents = gradient?.cgImage
self.setupGradientAnimations()
}
return size
}
private var badgeShapeAnimator: ConstantDisplayLinkAnimator?
private func animateBadgeTailPositionChange() {
if self.badgeShapeAnimator == nil {
self.badgeShapeAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.animateBadgeTailPositionChange()
})
self.badgeShapeAnimator?.isPaused = true
}
if let (startTime, duration, badgeSize, initial, target) = self.badgeShapeArguments {
self.badgeShapeAnimator?.isPaused = false
let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration)))
let value = initial + (target - initial) * t
let tailSize = CGSize(width: 15.0, height: 6.0)
let tailRadius: CGFloat = 3.0
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: value).cgPath
if t >= 1.0 {
self.badgeShapeArguments = nil
self.badgeShapeAnimator?.isPaused = true
self.badgeShapeAnimator?.invalidate()
self.badgeShapeAnimator = nil
}
} else {
self.badgeShapeAnimator?.isPaused = true
self.badgeShapeAnimator?.invalidate()
self.badgeShapeAnimator = nil
}
}
private func setupGradientAnimations() {
guard let _ = self.component else {
return
}
if let _ = self.badgeForeground.animation(forKey: "movement") {
} else {
CATransaction.begin()
let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0
let badgePreviousValue = self.badgeForeground.position.x
var badgeNewValue: CGFloat = badgeOffset
if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 {
badgeNewValue -= self.badgeForeground.frame.width * 0.35
}
self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0)
let lineOffset = 0.0
let linePreviousValue = self.activeBackground.position.x
var lineNewValue: CGFloat = lineOffset
if linePreviousValue < 0.0 {
lineNewValue = 0.0
} else {
lineNewValue = -self.activeContainer.bounds.width * 0.35
}
lineNewValue -= self.activeContainer.frame.minX
self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0)
let badgeAnimation = CABasicAnimation(keyPath: "position.x")
badgeAnimation.duration = 4.5
badgeAnimation.fromValue = badgePreviousValue
badgeAnimation.toValue = badgeNewValue
badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
CATransaction.setCompletionBlock { [weak self] in
self?.setupGradientAnimations()
}
self.badgeForeground.add(badgeAnimation, forKey: "movement")
let lineAnimation = CABasicAnimation(keyPath: "position.x")
lineAnimation.duration = 4.5
lineAnimation.fromValue = linePreviousValue
lineAnimation.toValue = lineNewValue
lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.activeBackground.add(lineAnimation, forKey: "movement")
CATransaction.commit()
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private let labelWidth: CGFloat = 10.0
private let labelHeight: CGFloat = 30.0
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
private let font = Font.with(size: 15.0, design: .regular, weight: .semibold, traits: [])
final class BadgeLabelView: UIView {
private class StackView: UIView {
var labels: [UILabel] = []
var currentValue: Int32 = 0
var color: UIColor = .white {
didSet {
for view in self.labels {
view.textColor = self.color
}
}
}
init() {
super.init(frame: CGRect(origin: .zero, size: labelSize))
var height: CGFloat = -labelHeight
for i in -1 ..< 10 {
let label = UILabel()
if i == -1 {
label.text = "9"
} else {
label.text = "\(i)"
}
label.textColor = self.color
label.font = font
label.textAlignment = .center
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
self.addSubview(label)
self.labels.append(label)
height += labelHeight
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
let previousValue = self.currentValue
self.currentValue = value
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
if previousValue == 9 && value < 9 {
self.bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: -1.0 * labelSize.height
),
size: labelSize
)
}
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
}
}
private var itemViews: [Int: StackView] = [:]
private var staticLabel = UILabel()
init() {
super.init(frame: .zero)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var color: UIColor = .white {
didSet {
self.staticLabel.textColor = self.color
for (_, view) in self.itemViews {
view.color = self.color
}
}
}
func update(value: String, transition: ComponentTransition) -> CGSize {
if value.contains(" ") {
for (_, view) in self.itemViews {
view.isHidden = true
}
if self.staticLabel.superview == nil {
self.staticLabel.textColor = self.color
self.staticLabel.font = font
self.addSubview(self.staticLabel)
}
self.staticLabel.text = value
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
}
let string = value
let stringArray = Array(string.map { String($0) }.reversed())
let totalWidth = CGFloat(stringArray.count) * labelWidth
var validIds: [Int] = []
for i in 0 ..< stringArray.count {
validIds.append(i)
let itemView: StackView
var itemTransition = transition
if let current = self.itemViews[i] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = StackView()
itemView.color = self.color
self.itemViews[i] = itemView
self.addSubview(itemView)
}
let digit = Int32(stringArray[i]) ?? 0
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
itemTransition.setFrame(
view: itemView,
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
)
}
var removeIds: [Int] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
itemView.removeFromSuperview()
})
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
return CGSize(width: totalWidth, height: labelHeight)
}
}

View file

@ -181,10 +181,12 @@ private final class GiftViewSheetContent: CombinedComponent {
let text: String?
let entities: [MessageTextEntity]?
let limitTotal: Int32?
var outgoing = false
var incoming = false
var savedToProfile = false
var converted = false
var giftId: Int64 = 0
var date: Int32 = 0
if let arguments = component.subject.arguments {
animationFile = arguments.gift.file
stars = arguments.gift.price
@ -192,10 +194,16 @@ private final class GiftViewSheetContent: CombinedComponent {
entities = arguments.entities
limitTotal = arguments.gift.availability?.total
convertStars = arguments.convertStars
if case .message = component.subject {
outgoing = !arguments.incoming
} else {
outgoing = false
}
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
savedToProfile = arguments.savedToProfile
converted = arguments.converted
giftId = arguments.gift.id
date = arguments.date
} else {
animationFile = nil
stars = 0
@ -235,11 +243,13 @@ private final class GiftViewSheetContent: CombinedComponent {
descriptionText = modifiedString
}
let formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator)
var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator)
if outgoing {
formattedAmount = "- \(formattedAmount)"
}
let countFont: UIFont = Font.semibold(17.0)
let amountText = formattedAmount
let countColor = theme.list.itemDisclosureActions.constructive.fillColor
let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor
let title = title.update(
component: MultilineTextComponent(
@ -331,7 +341,7 @@ private final class GiftViewSheetContent: CombinedComponent {
id: "date",
title: strings.Gift_View_Date,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: Int32(Date().timeIntervalSince1970), strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
@ -340,11 +350,13 @@ private final class GiftViewSheetContent: CombinedComponent {
if let gift = state.starGiftsMap[giftId], let availability = gift.availability {
remains = availability.remains
}
let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator)
tableItems.append(.init(
id: "availability",
title: strings.Gift_View_Availability,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "\(remains) of \(limitTotal)", font: tableFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor)))
)
))
}
@ -361,7 +373,8 @@ private final class GiftViewSheetContent: CombinedComponent {
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(attributedText)
text: .plain(attributedText),
maximumNumberOfLines: 0
)
)
))
@ -717,14 +730,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case message(EngineMessage)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action {
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted)
}
case let .profileGift(peerId, gift):
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
}
return nil
}
@ -1065,17 +1078,26 @@ private final class TableComponent: CombinedComponent {
} else {
insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding)
}
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append((valueChild, insets))
var titleHeight: CGFloat = 0.0
if let titleChild = updatedTitleChildren[i] {
titleHeight = titleChild.size.height
}
let availableValueWidth: CGFloat
if titleHeight > 0.0 {
availableValueWidth = rightColumnWidth
} else {
availableValueWidth = context.availableSize.width
}
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append((valueChild, insets))
let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0)
rowHeights[i] = rowHeight
totalHeight += rowHeight

View file

@ -5,7 +5,7 @@ import TelegramPresentationData
import AnimationUI
import Display
class DynamicIslandMaskNode: ASDisplayNode {
final class DynamicIslandMaskNode: ASDisplayNode {
var animationNode: AnimationNode?
var isForum = false {
@ -39,7 +39,7 @@ class DynamicIslandMaskNode: ASDisplayNode {
}
}
class DynamicIslandBlurNode: ASDisplayNode {
final class DynamicIslandBlurNode: ASDisplayNode {
private var effectView: UIVisualEffectView?
private let fadeNode = ASDisplayNode()
let gradientNode = ASImageNode()

View file

@ -1382,7 +1382,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
url: "",
simple: true,
source: .generic,
skipTermsOfService: true
skipTermsOfService: true,
payload: nil
)
})
}

View file

@ -138,8 +138,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize)
var index: Int32 = 0
for product in starsProducts {
let itemId = AnyHashable(product.date)
let itemId = AnyHashable(index)
validIds.append(itemId)
var itemTransition = transition
@ -221,6 +223,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
itemFrame.origin.x = sideInset
itemFrame.origin.y += starsOptionSize.height + optionSpacing
}
index += 1
}
var removeIds: [AnyHashable] = []
@ -243,7 +246,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.starsItems.removeValue(forKey: id)
}
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + 60.0 + 16.0
if self.peerId == self.context.account.peerId {
let transition = ComponentTransition.immediate

View file

@ -14,7 +14,7 @@ public final class StoryVideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init() {
self.contentContainerNode = ASDisplayNode()
@ -34,9 +34,9 @@ public final class StoryVideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -94,8 +94,8 @@ public final class StoryVideoDecoration: UniversalVideoDecoration {
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
@ -110,7 +110,7 @@ public final class StoryVideoDecoration: UniversalVideoDecoration {
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}

View file

@ -0,0 +1,197 @@
<!DOCTYPE html>
<html>
<head>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
#videoPlayer {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
object-fit: fill;
}
</style>
<script src="hls.js"></script>
<script type="text/javascript">
function postPlayerEvent(eventName, eventData) {
window.webkit.messageHandlers.performAction.postMessage({'event': eventName, 'data': eventData});
};
var video;
var hls;
var isManifestParsed = false;
var isFirstFrameReady = false;
var isPictureInPictureActive = false;
var currentTimeUpdateTimeout = null;
function playerInitialize(params) {
video = document.getElementById('videoPlayer');
video.addEventListener('loadeddata', (event) => {
if (!isFirstFrameReady) {
isFirstFrameReady = true;
refreshPlayerStatus();
}
});
video.addEventListener("playing", function() {
refreshPlayerStatus();
});
video.addEventListener("pause", function() {
refreshPlayerStatus();
});
video.addEventListener("seeking", function() {
refreshPlayerStatus();
});
video.addEventListener("waiting", function() {
refreshPlayerStatus();
});
video.addEventListener("enterpictureinpicture", function() {
isPictureInPictureActive = true;
refreshPlayerStatus();
}, false);
video.addEventListener("leavepictureinpicture", function() {
isPictureInPictureActive = false;
refreshPlayerStatus();
}, false);
hls = new Hls({
startLevel: 0,
testBandwidth: false,
debug: params['debug'],
autoStartLoad: false,
abrEwmaDefaultEstimate: params['bandwidthEstimate']
});
hls.on(Hls.Events.MANIFEST_PARSED, function() {
isManifestParsed = true;
refreshPlayerStatus();
});
hls.on(Hls.Events.LEVEL_SWITCHED, function() {
refreshPlayerStatus();
});
hls.on(Hls.Events.LEVELS_UPDATED, function() {
refreshPlayerStatus();
});
hls.loadSource('master.m3u8');
hls.attachMedia(video);
}
function playerLoad(initialLevelIndex) {
hls.startLevel = initialLevelIndex;
hls.startLoad(startPosition=-1);
}
function playerPlay() {
video.play();
}
function playerPause() {
video.pause();
}
function playerSetBaseRate(value) {
video.playbackRate = value;
}
function playerSetLevel(level) {
if (level >= 0) {
hls.autoLevelEnabled = false;
hls.currentLevel = level;
} else {
hls.autoLevelEnabled = true;
}
}
function playerSeek(value) {
video.currentTime = value;
}
function playerSetIsMuted(value) {
video.muted = value;
}
function playerRequestPictureInPicture() {
if (video !== document.pictureInPictureElement) {
video.requestPictureInPicture().then(function() {
isPictureInPictureActive = true;
refreshPlayerStatus();
});
}
}
function playerStopPictureInPicture() {
document.exitPictureInPicture();
}
function getLevels() {
var levels = [];
for (var i = 0; i < hls.levels.length; i++) {
level = hls.levels[i];
levels.push({
'index': i,
'bitrate': level.bitrate || 0,
'width': level.width || 0,
'height': level.height || 0
});
}
return levels;
}
function refreshPlayerStatus() {
var isPlaying = false;
if (!video.paused && !video.ended && video.readyState > 2) {
isPlaying = true;
}
postPlayerEvent('playerStatus', {
'isReady': isManifestParsed,
'isFirstFrameReady': isFirstFrameReady,
'isPlaying': !video.paused,
'rate': isPlaying ? video.playbackRate : 0.0,
'defaultRate': video.playbackRate,
'levels': getLevels(),
'currentLevel': hls.currentLevel,
'isPictureInPictureActive': isPictureInPictureActive
});
refreshPlayerCurrentTime();
if (isPlaying) {
if (currentTimeUpdateTimeout == null) {
currentTimeUpdateTimeout = setTimeout(() => {
refreshPlayerCurrentTime();
}, 200);
}
} else {
if(currentTimeUpdateTimeout != null){
clearTimeout(currentTimeUpdateTimeout);
currentTimeUpdateTimeout = null;
}
}
}
function refreshPlayerCurrentTime() {
postPlayerEvent('playerCurrentTime', {
'value': video.currentTime,
'bandwidthEstimate': hls.bandwidthEstimate
});
currentTimeUpdateTimeout = setTimeout(() => {
refreshPlayerCurrentTime()
}, 200);
}
</script>
</head>
<body>
<video id="videoPlayer" playsinline></video>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -945,8 +945,8 @@ final class AuthorizedApplicationContext {
chatLocation = .peer(peer)
}
if openAppIfAny, let parentController = self.rootController.viewControllers.last as? ViewController {
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true)
if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController {
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
}

View file

@ -14,7 +14,7 @@ import UndoUI
import UrlHandling
import TelegramPresentationData
func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) {
func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) {
let presentationData: PresentationData
if let parentController = parentController as? ChatControllerImpl {
presentationData = parentController.presentationData
@ -135,22 +135,16 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
}
}, didDismiss: { [weak parentController] in
if let parentController = parentController as? ChatControllerImpl {
// let isFocused = parentController.chatDisplayNode.textInputPanelNode?.isFocused ?? false
// parentController.chatDisplayNode.insertSubnode(parentController.chatDisplayNode.inputPanelContainerNode, aboveSubnode: parentController.chatDisplayNode.inputContextPanelContainer)
// if isFocused {
// parentController.chatDisplayNode.textInputPanelNode?.ensureFocused()
// }
parentController.updateChatPresentationInterfaceState(interactive: false) { state in
return state.updatedForceInputCommandsHidden(false)
}
}
}, getNavigationController: { [weak parentController] in
var navigationController: NavigationController?
if let parentController = parentController as? ChatControllerImpl {
return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
} else {
return parentController?.navigationController as? NavigationController
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
controller.navigationPresentation = .flatModal
parentController.push(controller)
@ -201,7 +195,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
} else {
source = url.isEmpty ? .generic : .simple
}
let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in
ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in
presentImpl?(c, a)
@ -209,11 +203,11 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
}, requestSwitchInline: { [weak parentController] query, chatTypes, completion in
ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
}, getNavigationController: { [weak parentController] in
var navigationController: NavigationController?
if let parentController = parentController as? ChatControllerImpl {
return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
} else {
return parentController?.navigationController as? NavigationController
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
controller.navigationPresentation = .flatModal
if let parentController = parentController as? ChatControllerImpl {
@ -257,11 +251,11 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
parentController.chatDisplayNode.historyNode.scrollToEndOfHistory()
}
}, getNavigationController: { [weak parentController] in
var navigationController: NavigationController?
if let parentController = parentController as? ChatControllerImpl {
return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
} else {
return parentController?.navigationController as? NavigationController
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
controller.navigationPresentation = .flatModal
if let parentController = parentController as? ChatControllerImpl {
@ -316,7 +310,7 @@ public extension ChatControllerImpl {
}
self.chatDisplayNode.dismissInput()
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false)
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil)
}
static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void {
@ -567,7 +561,7 @@ public extension ChatControllerImpl {
}
})
} else {
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false)
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload)
}
}
}

View file

@ -1172,7 +1172,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
strongSelf.push(wallpaperPreviewController)
return true
case let .giftPremium(_, _, duration, _, _):
case let .giftPremium(_, _, duration, _, _, _, _):
strongSelf.chatDisplayNode.dismissInput()
let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId
let toPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? message.id.peerId : strongSelf.context.account.peerId
@ -1187,7 +1187,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message))
strongSelf.push(controller)
return true
case let .giftCode(slug, _, _, _, _, _, _, _, _):
case let .giftCode(slug, _, _, _, _, _, _, _, _, _, _):
strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress)
return true
case .prizeStars:
@ -6946,12 +6946,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
self.stickerSettingsDisposable = combineLatest(queue: Queue.mainQueue(), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]), self.disableStickerAnimationsPromise.get()).startStrict(next: { [weak self] sharedData, disableStickerAnimations in
self.stickerSettingsDisposable = combineLatest(queue: Queue.mainQueue(),
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]),
self.disableStickerAnimationsPromise.get(),
context.sharedContext.hasGroupCallOnScreen
).startStrict(next: { [weak self] sharedData, disableStickerAnimations, hasGroupCallOnScreen in
var stickerSettings = StickerSettings.defaultSettings
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) {
stickerSettings = value
}
var disableStickerAnimations = disableStickerAnimations
if hasGroupCallOnScreen {
disableStickerAnimations = true
}
let chatStickerSettings = ChatInterfaceStickerSettings(stickerSettings: stickerSettings)
if let strongSelf = self, strongSelf.stickerSettings != chatStickerSettings || strongSelf.disableStickerAnimationsValue != disableStickerAnimations {
strongSelf.stickerSettings = chatStickerSettings
@ -9712,7 +9721,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
commit()
})
} else {
strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false)
strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload)
commit()
}
}

View file

@ -24,7 +24,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var contentNodeSnapshot: UIView?
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
init(tapped: @escaping () -> Void) {
self.tapped = tapped
@ -58,9 +58,9 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
self.progressNode.status = contentNode.status
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -74,15 +74,15 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
if let snapshot = snapshot {
self.contentContainerNode.view.addSubview(snapshot)
if let _ = self.validLayoutSize {
if let _ = self.validLayout {
snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size)
}
}
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
self.contentContainerNode.cornerRadius = size.width / 2.0
@ -96,7 +96,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(), size: size))
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: -0.5, dy: -0.5))
contentNode.updateLayout(size: size, transition: transition)
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
if let contentNodeSnapshot = self.contentNodeSnapshot {

View file

@ -2827,8 +2827,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData)
}
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService)
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload)
}
}

View file

@ -16,7 +16,7 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private let inset: CGFloat
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init(inset: CGFloat, backgroundImage: UIImage?, tapped: @escaping () -> Void) {
self.inset = inset
@ -51,9 +51,9 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -63,8 +63,8 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration {
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let diameter = size.width + inset
self.contentContainerNode.cornerRadius = (diameter - 3.0) / 2.0
@ -80,7 +80,7 @@ public final class ChatBubbleInstantVideoDecoration: UniversalVideoDecoration {
self.contentContainerNode.subnodeTransform = CATransform3DMakeScale((contentFrame.width + 2.0) / contentFrame.width, (contentFrame.width + 2.0) / contentFrame.width, 1.0)
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
contentNode.updateLayout(size: size, actualSize: actualSize, transition: transition)
}
}

View file

@ -23,7 +23,7 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration {
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
public init(corners: ImageCorners, nativeSize: CGSize, contentMode: ChatBubbleVideoDecorationContentMode, backgroundColor: UIColor) {
self.corners = corners
@ -82,23 +82,23 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let size = self.validLayoutSize {
if let validLayout = self.validLayout {
var scaledSize: CGSize
switch self.contentMode {
case .aspectFit:
scaledSize = self.nativeSize.aspectFitted(size)
scaledSize = self.nativeSize.aspectFitted(validLayout.size)
case .aspectFill:
scaledSize = self.nativeSize.aspectFilled(size)
scaledSize = self.nativeSize.aspectFilled(validLayout.size)
}
if abs(scaledSize.width - size.width) < 2.0 {
scaledSize.width = size.width
if abs(scaledSize.width - validLayout.size.width) < 2.0 {
scaledSize.width = validLayout.size.width
}
if abs(scaledSize.height - size.height) < 2.0 {
scaledSize.height = size.height
if abs(scaledSize.height - validLayout.size.height) < 2.0 {
scaledSize.height = validLayout.size.height
}
contentNode.frame = CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize)
contentNode.updateLayout(size: scaledSize, transition: .immediate)
contentNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.size.width - scaledSize.width) / 2.0), y: floor((validLayout.size.height - scaledSize.height) / 2.0)), size: scaledSize)
contentNode.updateLayout(size: scaledSize, actualSize: scaledSize, transition: .immediate)
}
}
}
@ -108,8 +108,8 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration {
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
public func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
@ -137,7 +137,7 @@ public final class ChatBubbleVideoDecoration: UniversalVideoDecoration {
scaledSize.height = size.height
}
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize))
contentNode.updateLayout(size: scaledSize, transition: transition)
contentNode.updateLayout(size: scaledSize, actualSize: scaledSize, transition: transition)
}
}

View file

@ -0,0 +1,549 @@
import Foundation
import SwiftSignalKit
import UniversalMediaPlayer
import Postbox
import TelegramCore
import AsyncDisplayKit
import AccountContext
import TelegramAudio
import RangeSet
import AVFoundation
import Display
import PhotoResources
import TelegramVoip
final class HLSVideoAVContentNode: ASDisplayNode, UniversalVideoContentNode {
private let postbox: Postbox
private let userLocation: MediaResourceUserLocation
private let fileReference: FileMediaReference
private let approximateDuration: Double
private let intrinsicDimensions: CGSize
private let audioSessionManager: ManagedAudioSession
private let audioSessionDisposable = MetaDisposable()
private var hasAudioSession = false
private let playbackCompletedListeners = Bag<() -> Void>()
private var initializedStatus = false
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
private var baseRate: Double = 1.0
private var isBuffering = false
private var seekId: Int = 0
private let _status = ValuePromise<MediaPlayerStatus>()
var status: Signal<MediaPlayerStatus, NoError> {
return self._status.get()
}
private let _bufferingStatus = Promise<(RangeSet<Int64>, Int64)?>()
var bufferingStatus: Signal<(RangeSet<Int64>, Int64)?, NoError> {
return self._bufferingStatus.get()
}
var isNativePictureInPictureActive: Signal<Bool, NoError> {
return .single(false)
}
private let _ready = Promise<Void>()
var ready: Signal<Void, NoError> {
return self._ready.get()
}
private let _preloadCompleted = ValuePromise<Bool>()
var preloadCompleted: Signal<Bool, NoError> {
return self._preloadCompleted.get()
}
private var playerSource: HLSServerSource?
private var serverDisposable: Disposable?
private let imageNode: TransformImageNode
private var playerItem: AVPlayerItem?
private var player: AVPlayer?
private let playerNode: ASDisplayNode
private var loadProgressDisposable: Disposable?
private var statusDisposable: Disposable?
private var didPlayToEndTimeObserver: NSObjectProtocol?
private var didBecomeActiveObserver: NSObjectProtocol?
private var willResignActiveObserver: NSObjectProtocol?
private var failureObserverId: NSObjectProtocol?
private var errorObserverId: NSObjectProtocol?
private var playerItemFailedToPlayToEndTimeObserver: NSObjectProtocol?
private let fetchDisposable = MetaDisposable()
private var dimensions: CGSize?
private let dimensionsPromise = ValuePromise<CGSize>(CGSize())
private var validLayout: (size: CGSize, actualSize: CGSize)?
private var statusTimer: Foundation.Timer?
private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto
init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) {
self.postbox = postbox
self.fileReference = fileReference
self.approximateDuration = fileReference.media.duration ?? 0.0
self.audioSessionManager = audioSessionManager
self.userLocation = userLocation
self.baseRate = baseRate
if var dimensions = fileReference.media.dimensions {
if let thumbnail = fileReference.media.previewRepresentations.first {
let dimensionsVertical = dimensions.width < dimensions.height
let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height
if dimensionsVertical != thumbnailVertical {
dimensions = PixelDimensions(width: dimensions.height, height: dimensions.width)
}
}
self.dimensions = dimensions.cgSize
} else {
self.dimensions = CGSize(width: 128.0, height: 128.0)
}
self.imageNode = TransformImageNode()
var player: AVPlayer?
player = AVPlayer(playerItem: nil)
self.player = player
if #available(iOS 16.0, *) {
player?.defaultRate = Float(baseRate)
}
if !enableSound {
player?.volume = 0.0
}
self.playerNode = ASDisplayNode()
self.playerNode.setLayerBlock({
return AVPlayerLayer(player: player)
})
self.intrinsicDimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 480.0, height: 320.0)
self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions)
if let qualitySet = HLSQualitySet(baseFile: fileReference) {
self.playerSource = HLSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: qualitySet.playlistFiles, qualityFiles: qualitySet.qualityFiles)
}
super.init()
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference) |> map { [weak self] getSize, getData in
Queue.mainQueue().async {
if let strongSelf = self, strongSelf.dimensions == nil {
if let dimensions = getSize() {
strongSelf.dimensions = dimensions
strongSelf.dimensionsPromise.set(dimensions)
if let validLayout = strongSelf.validLayout {
strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
}
return getData
})
self.addSubnode(self.imageNode)
self.addSubnode(self.playerNode)
self.player?.actionAtItemEnd = .pause
self.imageNode.imageUpdated = { [weak self] _ in
self?._ready.set(.single(Void()))
}
self.player?.addObserver(self, forKeyPath: "rate", options: [], context: nil)
self._bufferingStatus.set(.single(nil))
if let playerSource = self.playerSource {
self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource, completion: { [weak self] in
Queue.mainQueue().async {
guard let self else {
return
}
let playerItem: AVPlayerItem
let assetUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(playerSource.id)/master.m3u8"
#if DEBUG
print("HLSVideoAVContentNode: playing \(assetUrl)")
#endif
playerItem = AVPlayerItem(url: URL(string: assetUrl)!)
if #available(iOS 14.0, *) {
playerItem.startsOnFirstEligibleVariant = true
}
self.setPlayerItem(playerItem)
}
})
}
self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else {
return
}
layer.player = strongSelf.player
})
self.willResignActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in
guard let strongSelf = self, let layer = strongSelf.playerNode.layer as? AVPlayerLayer else {
return
}
layer.player = nil
})
}
deinit {
self.player?.removeObserver(self, forKeyPath: "rate")
self.setPlayerItem(nil)
self.audioSessionDisposable.dispose()
self.loadProgressDisposable?.dispose()
self.statusDisposable?.dispose()
if let didBecomeActiveObserver = self.didBecomeActiveObserver {
NotificationCenter.default.removeObserver(didBecomeActiveObserver)
}
if let willResignActiveObserver = self.willResignActiveObserver {
NotificationCenter.default.removeObserver(willResignActiveObserver)
}
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
}
if let failureObserverId = self.failureObserverId {
NotificationCenter.default.removeObserver(failureObserverId)
}
if let errorObserverId = self.errorObserverId {
NotificationCenter.default.removeObserver(errorObserverId)
}
self.serverDisposable?.dispose()
self.statusTimer?.invalidate()
}
private func setPlayerItem(_ item: AVPlayerItem?) {
if let playerItem = self.playerItem {
playerItem.removeObserver(self, forKeyPath: "playbackBufferEmpty")
playerItem.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
playerItem.removeObserver(self, forKeyPath: "playbackBufferFull")
playerItem.removeObserver(self, forKeyPath: "status")
playerItem.removeObserver(self, forKeyPath: "presentationSize")
}
if let playerItemFailedToPlayToEndTimeObserver = self.playerItemFailedToPlayToEndTimeObserver {
self.playerItemFailedToPlayToEndTimeObserver = nil
NotificationCenter.default.removeObserver(playerItemFailedToPlayToEndTimeObserver)
}
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
self.didPlayToEndTimeObserver = nil
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
}
if let failureObserverId = self.failureObserverId {
self.failureObserverId = nil
NotificationCenter.default.removeObserver(failureObserverId)
}
if let errorObserverId = self.errorObserverId {
self.errorObserverId = nil
NotificationCenter.default.removeObserver(errorObserverId)
}
self.playerItem = item
if let item {
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item, queue: nil, using: { [weak self] notification in
self?.performActionAtEnd()
})
self.failureObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.failedToPlayToEndTimeNotification, object: item, queue: .main, using: { notification in
#if DEBUG
print("Player Error: \(notification.description)")
#endif
})
self.errorObserverId = NotificationCenter.default.addObserver(forName: AVPlayerItem.newErrorLogEntryNotification, object: item, queue: .main, using: { [weak item] notification in
if let item {
let event = item.errorLog()?.events.last
if let event {
let _ = event
#if DEBUG
print("Player Error: \(event.errorComment ?? "<no comment>")")
#endif
}
}
})
item.addObserver(self, forKeyPath: "presentationSize", options: [], context: nil)
}
if let playerItem = self.playerItem {
playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .new, context: nil)
playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)
self.playerItemFailedToPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: playerItem, queue: OperationQueue.main, using: { [weak self] _ in
guard let self else {
return
}
let _ = self
})
}
self.player?.replaceCurrentItem(with: self.playerItem)
}
private func updateStatus() {
guard let player = self.player else {
return
}
let isPlaying = !player.rate.isZero
let status: MediaPlayerPlaybackStatus
if self.isBuffering {
status = .buffering(initial: false, whilePlaying: isPlaying, progress: 0.0, display: true)
} else {
status = isPlaying ? .playing : .paused
}
var timestamp = player.currentTime().seconds
if timestamp.isFinite && !timestamp.isNaN {
} else {
timestamp = 0.0
}
self.statusValue = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: self.baseRate, seekId: self.seekId, status: status, soundEnabled: true)
self._status.set(self.statusValue)
if case .playing = status {
if self.statusTimer == nil {
self.statusTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
self.updateStatus()
})
}
} else if let statusTimer = self.statusTimer {
self.statusTimer = nil
statusTimer.invalidate()
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate" {
if let player = self.player {
let isPlaying = !player.rate.isZero
if isPlaying {
self.isBuffering = false
}
}
self.updateStatus()
} else if keyPath == "playbackBufferEmpty" {
self.isBuffering = true
self.updateStatus()
} else if keyPath == "playbackLikelyToKeepUp" || keyPath == "playbackBufferFull" {
self.isBuffering = false
self.updateStatus()
} else if keyPath == "presentationSize" {
if let currentItem = self.player?.currentItem {
print("Presentation size: \(Int(currentItem.presentationSize.height))")
}
}
}
private func performActionAtEnd() {
for listener in self.playbackCompletedListeners.copyItems() {
listener()
}
}
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width)
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
if let dimensions = self.dimensions {
let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0))
let makeLayout = self.imageNode.asyncLayout()
let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear))
applyLayout()
}
}
func play() {
assert(Queue.mainQueue().isCurrent())
if !self.initializedStatus {
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.baseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true))
}
if !self.hasAudioSession {
if self.player?.volume != 0.0 {
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in
guard let self else {
return
}
self.hasAudioSession = true
self.player?.play()
}, deactivate: { [weak self] _ in
guard let self else {
return .complete()
}
self.hasAudioSession = false
self.player?.pause()
return .complete()
}))
} else {
self.player?.play()
}
} else {
self.player?.play()
}
}
func pause() {
assert(Queue.mainQueue().isCurrent())
self.player?.pause()
}
func togglePlayPause() {
assert(Queue.mainQueue().isCurrent())
guard let player = self.player else {
return
}
if player.rate.isZero {
self.play()
} else {
self.pause()
}
}
func setSoundEnabled(_ value: Bool) {
assert(Queue.mainQueue().isCurrent())
if value {
if !self.hasAudioSession {
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in
self?.hasAudioSession = true
self?.player?.volume = 1.0
}, deactivate: { [weak self] _ in
self?.hasAudioSession = false
self?.player?.pause()
return .complete()
}))
}
} else {
self.player?.volume = 0.0
self.hasAudioSession = false
self.audioSessionDisposable.set(nil)
}
}
func seek(_ timestamp: Double) {
assert(Queue.mainQueue().isCurrent())
self.seekId += 1
self.player?.seek(to: CMTime(seconds: timestamp, preferredTimescale: 30))
}
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
self.player?.volume = 1.0
self.play()
}
func setSoundMuted(soundMuted: Bool) {
self.player?.volume = soundMuted ? 0.0 : 1.0
}
func continueWithOverridingAmbientMode(isAmbient: Bool) {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
}
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
self.player?.volume = 0.0
self.hasAudioSession = false
self.audioSessionDisposable.set(nil)
}
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
}
func setBaseRate(_ baseRate: Double) {
guard let player = self.player else {
return
}
self.baseRate = baseRate
if #available(iOS 16.0, *) {
player.defaultRate = Float(baseRate)
}
if player.rate != 0.0 {
player.rate = Float(baseRate)
}
self.updateStatus()
}
func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
self.preferredVideoQuality = videoQuality
guard let currentItem = self.player?.currentItem else {
return
}
guard let playerSource = self.playerSource else {
return
}
switch videoQuality {
case .auto:
currentItem.preferredPeakBitRate = 0.0
case let .quality(qualityValue):
if let file = playerSource.qualityFiles[qualityValue] {
if let size = file.media.size, let duration = file.media.duration, duration != 0.0 {
let bandwidth = Int(Double(size) / duration) * 8
currentItem.preferredPeakBitRate = Double(bandwidth)
}
}
}
}
func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? {
guard let currentItem = self.player?.currentItem else {
return nil
}
guard let playerSource = self.playerSource else {
return nil
}
let current = Int(currentItem.presentationSize.height)
var available: [Int] = Array(playerSource.qualityFiles.keys)
available.sort(by: { $0 > $1 })
return (current, self.preferredVideoQuality, available)
}
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int {
return self.playbackCompletedListeners.add(f)
}
func removePlaybackCompleted(_ index: Int) {
self.playbackCompletedListeners.remove(index)
}
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
}
func notifyPlaybackControlsHidden(_ hidden: Bool) {
}
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
func enterNativePictureInPicture() -> Bool {
return false
}
func exitNativePictureInPicture() {
}
}

View file

@ -0,0 +1,777 @@
import Foundation
import SwiftSignalKit
import UniversalMediaPlayer
import Postbox
import TelegramCore
import WebKit
import AsyncDisplayKit
import AccountContext
import TelegramAudio
import Display
import PhotoResources
import TelegramVoip
import RangeSet
private func parseRange(from rangeString: String) -> Range<Int>? {
guard rangeString.hasPrefix("bytes=") else {
return nil
}
let rangeValues = rangeString.dropFirst("bytes=".count).split(separator: "-")
guard rangeValues.count == 2,
let start = Int(rangeValues[0]),
let end = Int(rangeValues[1]) else {
return nil
}
return start..<end + 1
}
private final class CustomVideoSchemeHandler: NSObject, WKURLSchemeHandler {
private final class PendingTask {
let sourceTask: any WKURLSchemeTask
let isCompleted = Atomic<Bool>(value: false)
var disposable: Disposable?
init(source: HLSServerSource, sourceTask: any WKURLSchemeTask) {
self.sourceTask = sourceTask
var requestRange: Range<Int>?
if let rangeString = sourceTask.request.allHTTPHeaderFields?["Range"] {
requestRange = parseRange(from: rangeString)
}
guard let url = sourceTask.request.url else {
return
}
let filePath = (url.absoluteString as NSString).lastPathComponent
if filePath == "master.m3u8" {
self.disposable = source.masterPlaylistData().startStrict(next: { [weak self] data in
guard let self else {
return
}
self.sendResponseAndClose(data: data.data(using: .utf8)!)
})
} else if filePath.hasPrefix("hls_level_") && filePath.hasSuffix(".m3u8") {
guard let levelIndex = Int(String(filePath[filePath.index(filePath.startIndex, offsetBy: "hls_level_".count) ..< filePath.index(filePath.endIndex, offsetBy: -".m3u8".count)])) else {
self.sendErrorAndClose()
return
}
self.disposable = source.playlistData(quality: levelIndex).startStrict(next: { [weak self] data in
guard let self else {
return
}
self.sendResponseAndClose(data: data.data(using: .utf8)!)
})
} else if filePath.hasPrefix("partfile") && filePath.hasSuffix(".mp4") {
let fileId = String(filePath[filePath.index(filePath.startIndex, offsetBy: "partfile".count) ..< filePath.index(filePath.endIndex, offsetBy: -".mp4".count)])
guard let fileIdValue = Int64(fileId) else {
self.sendErrorAndClose()
return
}
guard let requestRange else {
self.sendErrorAndClose()
return
}
self.disposable = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1)
|> take(1)).start(next: { [weak self] result in
guard let self else {
return
}
if let (file, range, totalSize) = result {
guard let allData = try? Data(contentsOf: URL(fileURLWithPath: file.path), options: .mappedIfSafe) else {
return
}
let data = allData.subdata(in: range)
self.sendResponseAndClose(data: data, range: requestRange, totalSize: totalSize)
} else {
self.sendErrorAndClose()
}
})
} else {
self.sendErrorAndClose()
}
}
deinit {
self.disposable?.dispose()
}
func cancel() {
}
func sendErrorAndClose() {
self.sourceTask.didFailWithError(NSError(domain: "LocalVideoError", code: 500, userInfo: nil))
}
private func sendResponseAndClose(data: Data, range: Range<Int>? = nil, totalSize: Int? = nil) {
// Create the response with the appropriate content-type and content-length
//let mimeType = "application/octet-stream"
let responseLength = data.count
// Construct URLResponse with optional range headers (for partial content responses)
var headers: [String: String] = [
"Content-Length": "\(responseLength)",
"Connection": "close",
"Access-Control-Allow-Origin": "*"
]
if let range = range, let totalSize = totalSize {
headers["Content-Range"] = "bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)"
}
// Create the URLResponse object
let response = HTTPURLResponse(url: self.sourceTask.request.url!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: headers)
// Send the response headers
self.sourceTask.didReceive(response!)
// Send the response data
self.sourceTask.didReceive(data)
// Complete the task
self.sourceTask.didFinish()
}
}
private let source: HLSServerSource
private var pendingTasks: [PendingTask] = []
init(source: HLSServerSource) {
self.source = source
}
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
self.pendingTasks.append(PendingTask(source: self.source, sourceTask: urlSchemeTask))
}
func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) {
if let index = self.pendingTasks.firstIndex(where: { $0.sourceTask === urlSchemeTask }) {
let task = self.pendingTasks[index]
self.pendingTasks.remove(at: index)
task.cancel()
}
}
}
private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
private let f: (WKScriptMessage) -> ()
init(_ f: @escaping (WKScriptMessage) -> ()) {
self.f = f
super.init()
}
func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) {
self.f(scriptMessage)
}
}
final class HLSVideoJSContentNode: ASDisplayNode, UniversalVideoContentNode {
private struct Level {
let bitrate: Int
let width: Int
let height: Int
init(bitrate: Int, width: Int, height: Int) {
self.bitrate = bitrate
self.width = width
self.height = height
}
}
private static var sharedBandwidthEstimate: Double?
private let postbox: Postbox
private let userLocation: MediaResourceUserLocation
private let fileReference: FileMediaReference
private let approximateDuration: Double
private let intrinsicDimensions: CGSize
private let audioSessionManager: ManagedAudioSession
private let audioSessionDisposable = MetaDisposable()
private var hasAudioSession = false
private let playerSource: HLSServerSource?
private var serverDisposable: Disposable?
private let playbackCompletedListeners = Bag<() -> Void>()
private var initializedStatus = false
private var statusValue = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
private var isBuffering = false
private var seekId: Int = 0
private let _status = ValuePromise<MediaPlayerStatus>()
var status: Signal<MediaPlayerStatus, NoError> {
return self._status.get()
}
private let _bufferingStatus = Promise<(RangeSet<Int64>, Int64)?>()
var bufferingStatus: Signal<(RangeSet<Int64>, Int64)?, NoError> {
return self._bufferingStatus.get()
}
private let _isNativePictureInPictureActive = ValuePromise<Bool>(false, ignoreRepeated: true)
var isNativePictureInPictureActive: Signal<Bool, NoError> {
return self._isNativePictureInPictureActive.get()
}
private let _ready = Promise<Void>()
var ready: Signal<Void, NoError> {
return self._ready.get()
}
private let _preloadCompleted = ValuePromise<Bool>()
var preloadCompleted: Signal<Bool, NoError> {
return self._preloadCompleted.get()
}
private let imageNode: TransformImageNode
private let webView: WKWebView
private let fetchDisposable = MetaDisposable()
private var dimensions: CGSize?
private let dimensionsPromise = ValuePromise<CGSize>(CGSize())
private var validLayout: (size: CGSize, actualSize: CGSize)?
private var statusTimer: Foundation.Timer?
private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto
private var playerIsReady: Bool = false
private var playerIsFirstFrameReady: Bool = false
private var playerIsPlaying: Bool = false
private var playerRate: Double = 0.0
private var playerDefaultRate: Double = 1.0
private var playerTime: Double = 0.0
private var playerTimeGenerationTimestamp: Double = 0.0
private var playerAvailableLevels: [Int: Level] = [:]
private var playerCurrentLevelIndex: Int?
private var hasRequestedPlayerLoad: Bool = false
private var requestedPlaying: Bool = false
private var requestedBaseRate: Double = 1.0
private var requestedLevelIndex: Int?
init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) {
self.postbox = postbox
self.fileReference = fileReference
self.approximateDuration = fileReference.media.duration ?? 0.0
self.audioSessionManager = audioSessionManager
self.userLocation = userLocation
self.requestedBaseRate = baseRate
/*#if DEBUG
if let minimizedQualityFile = HLSVideoContent.minimizedHLSQualityFile(file: self.fileReference) {
let _ = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: minimizedQualityFile.resourceReference(minimizedQualityFile.media.resource), range: (0 ..< 5 * 1024 * 1024, .default)).startStandalone()
}
#endif*/
if var dimensions = fileReference.media.dimensions {
if let thumbnail = fileReference.media.previewRepresentations.first {
let dimensionsVertical = dimensions.width < dimensions.height
let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height
if dimensionsVertical != thumbnailVertical {
dimensions = PixelDimensions(width: dimensions.height, height: dimensions.width)
}
}
self.dimensions = dimensions.cgSize
} else {
self.dimensions = CGSize(width: 128.0, height: 128.0)
}
self.imageNode = TransformImageNode()
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
config.allowsPictureInPictureMediaPlayback = true
var playerSource: HLSServerSource?
if let qualitySet = HLSQualitySet(baseFile: fileReference) {
let playerSourceValue = HLSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: qualitySet.playlistFiles, qualityFiles: qualitySet.qualityFiles)
playerSource = playerSourceValue
let schemeHandler = CustomVideoSchemeHandler(source: playerSourceValue)
config.setURLSchemeHandler(schemeHandler, forURLScheme: "tghls")
}
self.playerSource = playerSource
let userController = WKUserContentController()
var handleScriptMessage: ((WKScriptMessage) -> Void)?
userController.add(WeakScriptMessageHandler { message in
handleScriptMessage?(message)
}, name: "performAction")
let isDebug: Bool
#if DEBUG
isDebug = true
#else
isDebug = false
#endif
let mediaDimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 480.0, height: 320.0)
var intrinsicDimensions = mediaDimensions.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0))
let userScriptJs = """
playerInitialize({
'debug': \(isDebug),
'width': \(Int(intrinsicDimensions.width)),
'height': \(Int(intrinsicDimensions.height)),
'bandwidthEstimate': \(HLSVideoJSContentNode.sharedBandwidthEstimate ?? 500000.0)
});
""";
let userScript = WKUserScript(source: userScriptJs, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
userController.addUserScript(userScript)
config.userContentController = userController
intrinsicDimensions.width = floor(intrinsicDimensions.width / UIScreenScale)
intrinsicDimensions.height = floor(intrinsicDimensions.height / UIScreenScale)
self.intrinsicDimensions = intrinsicDimensions
self.webView = WKWebView(frame: CGRect(origin: CGPoint(), size: self.intrinsicDimensions), configuration: config)
self.webView.scrollView.isScrollEnabled = false
self.webView.allowsLinkPreview = false
self.webView.allowsBackForwardNavigationGestures = false
self.webView.accessibilityIgnoresInvertColors = true
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
self.webView.alpha = 0.0
if #available(iOS 16.4, *) {
#if DEBUG
self.webView.isInspectable = true
#endif
}
super.init()
self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: fileReference) |> map { [weak self] getSize, getData in
Queue.mainQueue().async {
if let strongSelf = self, strongSelf.dimensions == nil {
if let dimensions = getSize() {
strongSelf.dimensions = dimensions
strongSelf.dimensionsPromise.set(dimensions)
if let validLayout = strongSelf.validLayout {
strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
}
return getData
})
self.addSubnode(self.imageNode)
self.view.addSubview(self.webView)
self.imageNode.imageUpdated = { [weak self] _ in
self?._ready.set(.single(Void()))
}
self._bufferingStatus.set(.single(nil))
handleScriptMessage = { [weak self] message in
Queue.mainQueue().async {
guard let self else {
return
}
guard let body = message.body as? [String: Any] else {
return
}
guard let eventName = body["event"] as? String else {
return
}
switch eventName {
case "playerStatus":
guard let eventData = body["data"] as? [String: Any] else {
return
}
if let isReady = eventData["isReady"] as? Bool {
self.playerIsReady = isReady
} else {
self.playerIsReady = false
}
if let isFirstFrameReady = eventData["isFirstFrameReady"] as? Bool {
self.playerIsFirstFrameReady = isFirstFrameReady
} else {
self.playerIsFirstFrameReady = false
}
if let isPlaying = eventData["isPlaying"] as? Bool {
self.playerIsPlaying = isPlaying
} else {
self.playerIsPlaying = false
}
if let rate = eventData["rate"] as? Double {
self.playerRate = rate
} else {
self.playerRate = 0.0
}
if let defaultRate = eventData["defaultRate"] as? Double {
self.playerDefaultRate = defaultRate
} else {
self.playerDefaultRate = 0.0
}
if let levels = eventData["levels"] as? [[String: Any]] {
self.playerAvailableLevels.removeAll()
for level in levels {
guard let levelIndex = level["index"] as? Int else {
continue
}
guard let levelBitrate = level["bitrate"] as? Int else {
continue
}
guard let levelWidth = level["width"] as? Int else {
continue
}
guard let levelHeight = level["height"] as? Int else {
continue
}
self.playerAvailableLevels[levelIndex] = Level(
bitrate: levelBitrate,
width: levelWidth,
height: levelHeight
)
}
} else {
self.playerAvailableLevels.removeAll()
}
self._isNativePictureInPictureActive.set(eventData["isPictureInPictureActive"] as? Bool ?? false)
if let currentLevel = eventData["currentLevel"] as? Int {
if self.playerAvailableLevels[currentLevel] != nil {
self.playerCurrentLevelIndex = currentLevel
} else {
self.playerCurrentLevelIndex = nil
}
} else {
self.playerCurrentLevelIndex = nil
}
self.webView.alpha = self.playerIsFirstFrameReady ? 1.0 : 0.0
if self.playerIsReady {
if !self.hasRequestedPlayerLoad {
if !self.playerAvailableLevels.isEmpty {
var selectedLevelIndex: Int?
if let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file {
if let dimensions = minimizedQualityFile.media.dimensions {
for (index, level) in self.playerAvailableLevels {
if level.height == Int(dimensions.height) {
selectedLevelIndex = index
break
}
}
}
}
if selectedLevelIndex == nil {
selectedLevelIndex = self.playerAvailableLevels.sorted(by: { $0.value.height > $1.value.height }).first?.key
}
if let selectedLevelIndex {
self.hasRequestedPlayerLoad = true
self.webView.evaluateJavaScript("playerLoad(\(selectedLevelIndex));", completionHandler: nil)
}
}
}
self.webView.evaluateJavaScript("playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil)
if self.requestedPlaying {
self.requestPlay()
} else {
self.requestPause()
}
}
self.updateStatus()
case "playerCurrentTime":
guard let eventData = body["data"] as? [String: Any] else {
return
}
guard let value = eventData["value"] as? Double else {
return
}
self.playerTime = value
self.playerTimeGenerationTimestamp = CACurrentMediaTime()
var bandwidthEstimate = eventData["bandwidthEstimate"] as? Double
if let bandwidthEstimateValue = bandwidthEstimate, bandwidthEstimateValue.isNaN || bandwidthEstimateValue.isInfinite {
bandwidthEstimate = nil
}
HLSVideoJSContentNode.sharedBandwidthEstimate = bandwidthEstimate
self.updateStatus()
default:
break
}
}
}
if let playerSource = self.playerSource {
self.serverDisposable = SharedHLSServer.shared.registerPlayer(source: playerSource, completion: { [weak self] in
Queue.mainQueue().async {
guard let self else {
return
}
let htmlUrl = "http://127.0.0.1:\(SharedHLSServer.shared.port)/\(playerSource.id)/index.html"
self.webView.load(URLRequest(url: URL(string: htmlUrl)!))
}
})
}
}
deinit {
self.serverDisposable?.dispose()
self.audioSessionDisposable.dispose()
self.statusTimer?.invalidate()
}
private func updateStatus() {
let isPlaying = self.requestedPlaying && self.playerRate != 0.0
let status: MediaPlayerPlaybackStatus
if self.requestedPlaying && !isPlaying {
status = .buffering(initial: false, whilePlaying: self.requestedPlaying, progress: 0.0, display: true)
} else {
status = self.requestedPlaying ? .playing : .paused
}
var timestamp = self.playerTime
if timestamp.isFinite && !timestamp.isNaN {
} else {
timestamp = 0.0
}
self.statusValue = MediaPlayerStatus(generationTimestamp: self.playerTimeGenerationTimestamp, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: timestamp, baseRate: self.requestedBaseRate, seekId: self.seekId, status: status, soundEnabled: true)
self._status.set(self.statusValue)
if case .playing = status {
if self.statusTimer == nil {
self.statusTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
self.updateStatus()
})
}
} else if let statusTimer = self.statusTimer {
self.statusTimer = nil
statusTimer.invalidate()
}
}
private func performActionAtEnd() {
for listener in self.playbackCompletedListeners.copyItems() {
listener()
}
}
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
transition.updatePosition(layer: self.webView.layer, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
transition.updateTransformScale(layer: self.webView.layer, scale: size.width / self.intrinsicDimensions.width)
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
if let dimensions = self.dimensions {
let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0))
let makeLayout = self.imageNode.asyncLayout()
let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear))
applyLayout()
}
}
func play() {
assert(Queue.mainQueue().isCurrent())
if !self.initializedStatus {
self._status.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: Double(self.approximateDuration), dimensions: CGSize(), timestamp: 0.0, baseRate: self.requestedBaseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true))
}
/*if !self.hasAudioSession {
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in
Queue.mainQueue().async {
guard let self else {
return
}
self.hasAudioSession = true
self.requestPlay()
}
}, deactivate: { [weak self] _ in
return Signal { subscriber in
if let self {
self.hasAudioSession = false
self.requestPause()
}
subscriber.putCompletion()
return EmptyDisposable
}
|> runOn(.mainQueue())
}))
} else*/ do {
self.requestPlay()
}
}
private func requestPlay() {
self.requestedPlaying = true
if self.playerIsReady {
self.webView.evaluateJavaScript("playerPlay();", completionHandler: nil)
}
self.updateStatus()
}
private func requestPause() {
self.requestedPlaying = false
if self.playerIsReady {
self.webView.evaluateJavaScript("playerPause();", completionHandler: nil)
}
self.updateStatus()
}
func pause() {
assert(Queue.mainQueue().isCurrent())
self.requestPause()
}
func togglePlayPause() {
assert(Queue.mainQueue().isCurrent())
if self.requestedPlaying {
self.pause()
} else {
self.play()
}
}
func setSoundEnabled(_ value: Bool) {
assert(Queue.mainQueue().isCurrent())
/*if value {
if !self.hasAudioSession {
self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play(mixWithOthers: false), activate: { [weak self] _ in
self?.hasAudioSession = true
self?.player?.volume = 1.0
}, deactivate: { [weak self] _ in
self?.hasAudioSession = false
self?.player?.pause()
return .complete()
}))
}
} else {
self.player?.volume = 0.0
self.hasAudioSession = false
self.audioSessionDisposable.set(nil)
}*/
}
func seek(_ timestamp: Double) {
assert(Queue.mainQueue().isCurrent())
self.seekId += 1
self.webView.evaluateJavaScript("playerSeek(\(timestamp));", completionHandler: nil)
}
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
self.webView.evaluateJavaScript("playerSetIsMuted(false);", completionHandler: nil)
self.play()
}
func setSoundMuted(soundMuted: Bool) {
self.webView.evaluateJavaScript("playerSetIsMuted(\(soundMuted));", completionHandler: nil)
}
func continueWithOverridingAmbientMode(isAmbient: Bool) {
}
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) {
}
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
self.webView.evaluateJavaScript("playerSetIsMuted(true);", completionHandler: nil)
self.hasAudioSession = false
self.audioSessionDisposable.set(nil)
}
func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) {
}
func setBaseRate(_ baseRate: Double) {
self.requestedBaseRate = baseRate
if self.playerIsReady {
self.webView.evaluateJavaScript("playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil)
}
self.updateStatus()
}
func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) {
self.preferredVideoQuality = videoQuality
switch videoQuality {
case .auto:
self.requestedLevelIndex = nil
case let .quality(quality):
if let level = self.playerAvailableLevels.first(where: { $0.value.height == quality }) {
self.requestedLevelIndex = level.key
} else {
self.requestedLevelIndex = nil
}
}
if self.playerIsReady {
self.webView.evaluateJavaScript("playerSetLevel(\(self.requestedLevelIndex ?? -1));", completionHandler: nil)
}
}
func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? {
guard let playerCurrentLevelIndex = self.playerCurrentLevelIndex else {
return nil
}
guard let currentLevel = self.playerAvailableLevels[playerCurrentLevelIndex] else {
return nil
}
var available = self.playerAvailableLevels.values.map(\.height)
available.sort(by: { $0 > $1 })
return (currentLevel.height, self.preferredVideoQuality, available)
}
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int {
return self.playbackCompletedListeners.add(f)
}
func removePlaybackCompleted(_ index: Int) {
self.playbackCompletedListeners.remove(index)
}
func fetchControl(_ control: UniversalVideoNodeFetchControl) {
}
func notifyPlaybackControlsHidden(_ hidden: Bool) {
}
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
func enterNativePictureInPicture() -> Bool {
self.webView.evaluateJavaScript("playerRequestPictureInPicture();", completionHandler: nil)
return true
}
func exitNativePictureInPicture() {
self.webView.evaluateJavaScript("playerStopPictureInPicture();", completionHandler: nil)
}
}

View file

@ -206,6 +206,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
return self._bufferingStatus.get()
}
var isNativePictureInPictureActive: Signal<Bool, NoError> {
return .single(false)
}
private let _ready = Promise<Void>()
var ready: Signal<Void, NoError> {
return self._ready.get()
@ -217,7 +221,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
private var dimensions: CGSize?
private let dimensionsPromise = ValuePromise<CGSize>(CGSize())
private var validLayout: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
private var shouldPlay: Bool = false
@ -306,8 +310,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
if let dimensions = getSize() {
strongSelf.dimensions = dimensions
strongSelf.dimensionsPromise.set(dimensions)
if let size = strongSelf.validLayout {
strongSelf.updateLayout(size: size, transition: .immediate)
if let validLayout = strongSelf.validLayout {
strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -432,8 +436,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
if let dimensions = self.dimensions {
let imageSize = CGSize(width: floor(dimensions.width / 2.0), height: floor(dimensions.height / 2.0))
@ -685,4 +689,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy)
}
func enterNativePictureInPicture() -> Bool {
return false
}
func exitNativePictureInPicture() {
}
}

View file

@ -47,7 +47,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
private let statusDisposable = MetaDisposable()
private var validLayoutSize: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
init(contentDimensions: CGSize, unminimize: @escaping () -> Void, togglePlayPause: @escaping () -> Void, expand: @escaping () -> Void, close: @escaping () -> Void, controlsAreShowingUpdated: @escaping (Bool) -> Void) {
self.contentDimensions = contentDimensions
@ -106,9 +106,9 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = self.frameForContent(size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
if let validLayout = self.validLayout {
contentNode.frame = self.frameForContent(size: validLayout.size)
contentNode.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -118,8 +118,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, actualSize)
let contentFrame = self.frameForContent(size: size)
@ -146,7 +146,7 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: contentFrame)
contentNode.updateLayout(size: contentFrame.size, transition: transition)
contentNode.updateLayout(size: contentFrame.size, actualSize: contentFrame.size, transition: transition)
}
}
@ -209,8 +209,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
if self.minimizedBlurView == nil {
let minimizedBlurView = UIVisualEffectView(effect: nil)
self.minimizedBlurView = minimizedBlurView
if let validLayoutSize = self.validLayoutSize {
minimizedBlurView.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
if let validLayout = self.validLayout {
minimizedBlurView.frame = CGRect(origin: CGPoint(), size: validLayout.size)
}
minimizedBlurView.isHidden = true
self.foregroundContainerNode.view.addSubview(minimizedBlurView)
@ -222,8 +222,8 @@ final class OverlayVideoDecoration: UniversalVideoDecoration {
self.minimizedBlurView?.contentView.addSubview(minimizedArrowView)
}
if let minimizedArrowView = self.minimizedArrowView {
if let validLayoutSize = self.validLayoutSize {
setupArrowFrame(size: validLayoutSize, edge: edge, view: minimizedArrowView)
if let validLayout = self.validLayout {
setupArrowFrame(size: validLayout.size, edge: edge, view: minimizedArrowView)
}
minimizedArrowView.setAngled(!adjusting, animated: true)
}

View file

@ -141,6 +141,10 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
return self._bufferingStatus.get()
}
var isNativePictureInPictureActive: Signal<Bool, NoError> {
return .single(false)
}
private let _ready = Promise<Void>()
var ready: Signal<Void, NoError> {
return self._ready.get()
@ -170,7 +174,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
private var dimensions: CGSize?
private let dimensionsPromise = ValuePromise<CGSize>(CGSize())
private var validLayout: CGSize?
private var validLayout: (size: CGSize, actualSize: CGSize)?
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, content: PlatformVideoContent.Content, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) {
self.postbox = postbox
@ -203,8 +207,8 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
if let dimensions = getSize() {
strongSelf.dimensions = dimensions
strongSelf.dimensionsPromise.set(dimensions)
if let size = strongSelf.validLayout {
strongSelf.updateLayout(size: size, transition: .immediate)
if let validLayout = strongSelf.validLayout {
strongSelf.updateLayout(size: validLayout.size, actualSize: validLayout.actualSize, transition: .immediate)
}
}
}
@ -371,7 +375,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width)
@ -471,4 +475,11 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
func enterNativePictureInPicture() -> Bool {
return false
}
func exitNativePictureInPicture() {
}
}

View file

@ -58,6 +58,10 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
return self._bufferingStatus.get()
}
var isNativePictureInPictureActive: Signal<Bool, NoError> {
return .single(false)
}
private let _ready = Promise<Void>()
var ready: Signal<Void, NoError> {
return self._ready.get()
@ -207,7 +211,7 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width)
@ -308,5 +312,12 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
func enterNativePictureInPicture() -> Bool {
return false
}
func exitNativePictureInPicture() {
}
}

View file

@ -31,9 +31,12 @@ private final class UniversalVideoContentHolder {
var bufferingStatusDisposable: Disposable?
var bufferingStatusValue: (RangeSet<Int64>, Int64)?
var isNativePictureInPictureActiveDisposable: Disposable?
var isNativePictureInPictureActiveValue: Bool = false
var playbackCompletedIndex: Int?
init(content: UniversalVideoContent, contentNode: UniversalVideoContentNode & ASDisplayNode, statusUpdated: @escaping (MediaPlayerStatus?) -> Void, bufferingStatusUpdated: @escaping ((RangeSet<Int64>, Int64)?) -> Void, playbackCompleted: @escaping () -> Void) {
init(content: UniversalVideoContent, contentNode: UniversalVideoContentNode & ASDisplayNode, statusUpdated: @escaping (MediaPlayerStatus?) -> Void, bufferingStatusUpdated: @escaping ((RangeSet<Int64>, Int64)?) -> Void, playbackCompleted: @escaping () -> Void, isNativePictureInPictureActiveUpdated: @escaping (Bool) -> Void) {
self.content = content
self.contentNode = contentNode
@ -51,6 +54,13 @@ private final class UniversalVideoContentHolder {
}
})
self.isNativePictureInPictureActiveDisposable = (contentNode.isNativePictureInPictureActive |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
strongSelf.isNativePictureInPictureActiveValue = value
isNativePictureInPictureActiveUpdated(value)
}
})
self.playbackCompletedIndex = contentNode.addPlaybackCompleted {
playbackCompleted()
}
@ -59,6 +69,7 @@ private final class UniversalVideoContentHolder {
deinit {
self.statusDisposable?.dispose()
self.bufferingStatusDisposable?.dispose()
self.isNativePictureInPictureActiveDisposable?.dispose()
if let playbackCompletedIndex = self.playbackCompletedIndex {
self.contentNode.removePlaybackCompleted(playbackCompletedIndex)
}
@ -133,9 +144,10 @@ private final class UniversalVideoContentHolderCallbacks {
let playbackCompleted = Bag<() -> Void>()
let status = Bag<(MediaPlayerStatus?) -> Void>()
let bufferingStatus = Bag<((RangeSet<Int64>, Int64)?) -> Void>()
let isNativePictureInPictureActive = Bag<(Bool) -> Void>()
var isEmpty: Bool {
return self.playbackCompleted.isEmpty && self.status.isEmpty && self.bufferingStatus.isEmpty
return self.playbackCompleted.isEmpty && self.status.isEmpty && self.bufferingStatus.isEmpty && self.isNativePictureInPictureActive.isEmpty
}
}
@ -190,6 +202,14 @@ public final class UniversalVideoManagerImpl: UniversalVideoManager {
}
}
}
}, isNativePictureInPictureActiveUpdated: { [weak self] value in
if let strongSelf = self {
if let current = strongSelf.holderCallbacks[content.id] {
for subscriber in current.isNativePictureInPictureActive.copyItems() {
subscriber(value)
}
}
}
})
self.holders[content.id] = holder
}
@ -311,4 +331,37 @@ public final class UniversalVideoManagerImpl: UniversalVideoManager {
}
} |> runOn(Queue.mainQueue())
}
public func isNativePictureInPictureActiveSignal(content: UniversalVideoContent) -> Signal<Bool, NoError> {
return Signal { subscriber in
var callbacks: UniversalVideoContentHolderCallbacks
if let current = self.holderCallbacks[content.id] {
callbacks = current
} else {
callbacks = UniversalVideoContentHolderCallbacks()
self.holderCallbacks[content.id] = callbacks
}
let index = callbacks.isNativePictureInPictureActive.add({ value in
subscriber.putNext(value)
})
if let current = self.holders[content.id] {
subscriber.putNext(current.isNativePictureInPictureActiveValue)
} else {
subscriber.putNext(false)
}
return ActionDisposable {
Queue.mainQueue().async {
if let current = self.holderCallbacks[content.id] {
current.status.remove(index)
if current.playbackCompleted.isEmpty {
self.holderCallbacks.removeValue(forKey: content.id)
}
}
}
}
} |> runOn(Queue.mainQueue())
}
}

View file

@ -227,4 +227,11 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
func enterNativePictureInPicture() -> Bool {
return false
}
func exitNativePictureInPicture() {
}
}

View file

@ -58,6 +58,10 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
return self._bufferingStatus.get()
}
var isNativePictureInPictureActive: Signal<Bool, NoError> {
return .single(false)
}
private var seekId: Int = 0
private let _ready = Promise<Void>()
@ -116,7 +120,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
self.readyDisposable.dispose()
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, actualSize: CGSize, transition: ContainedViewLayoutTransition) {
transition.updatePosition(node: self.playerNode, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
transition.updateTransformScale(node: self.playerNode, scale: size.width / self.intrinsicDimensions.width)
@ -207,4 +211,11 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
}
func enterNativePictureInPicture() -> Bool {
return false
}
func exitNativePictureInPicture() {
}
}

View file

@ -336,6 +336,10 @@ public final class ExternalMediaStreamingContext: SharedHLSServerSource {
impl.fileData(id: id, range: range).start(next: subscriber.putNext)
}
}
public func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> {
return .single(nil)
}
}
public protocol SharedHLSServerSource: AnyObject {
@ -345,6 +349,7 @@ public protocol SharedHLSServerSource: AnyObject {
func playlistData(quality: Int) -> Signal<String, NoError>
func partData(index: Int, quality: Int) -> Signal<Data?, NoError>
func fileData(id: Int64, range: Range<Int>) -> Signal<(TempBoxFile, Range<Int>, Int)?, NoError>
func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError>
}
@available(iOS 12.0, macOS 14.0, *)
@ -651,7 +656,19 @@ public final class SharedHLSServer {
}
})
} else {
self.sendErrorAndClose(connection: connection, error: .notFound)
let _ = (source.arbitraryFileData(path: filePath)
|> deliverOn(self.queue)
|> take(1)).start(next: { [weak self] result in
guard let self else {
return
}
if let result {
self.sendResponseAndClose(connection: connection, data: result.data, contentType: result.contentType)
} else {
self.sendErrorAndClose(connection: connection, error: .notFound)
}
})
}
}
@ -665,13 +682,14 @@ public final class SharedHLSServer {
})
}
private func sendResponseAndClose(connection: NWConnection, data: Data, range: Range<Int>? = nil, totalSize: Int? = nil) {
private func sendResponseAndClose(connection: NWConnection, data: Data, contentType: String = "application/octet-stream", range: Range<Int>? = nil, totalSize: Int? = nil) {
var responseHeaders = "HTTP/1.1 200 OK\r\n"
responseHeaders.append("Content-Length: \(data.count)\r\n")
if let range, let totalSize {
responseHeaders.append("Content-Range: bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)\r\n")
}
responseHeaders.append("Content-Type: application/octet-stream\r\n")
responseHeaders.append("Content-Type: \(contentType)\r\n")
responseHeaders.append("Connection: close\r\n")
responseHeaders.append("Access-Control-Allow-Origin: *\r\n")
responseHeaders.append("\r\n")

@ -1 +1 @@
Subproject commit 846f7040480f52b8bc0382fb9e2e78e8ef60c633
Subproject commit b6e7349b98c5d3999f45e9468eee068aff86ee37

View file

@ -1,5 +1,5 @@
{
"app": "11.2",
"app": "11.2.1",
"xcode": "16.0",
"bazel": "7.3.1",
"macos": "15.0"