mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit 'f174769d2d' into beta
This commit is contained in:
commit
525f8cc179
90 changed files with 4489 additions and 1385 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -65,3 +65,7 @@ xcodeproj.bazelrc
|
|||
build-input/*
|
||||
**/*.pyc
|
||||
*.pyc
|
||||
submodules/OpusBinding/SharedHeaders/*
|
||||
submodules/FFMpegBinding/SharedHeaders/*
|
||||
submodules/OpenSSLEncryptionProvider/SharedHeaders/*
|
||||
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ let package = Package(
|
|||
publicHeadersPath: "Public",
|
||||
cSettings: [
|
||||
.headerSearchPath("Public"),
|
||||
.unsafeFlags(["-I../../../../core-xprojects/ffmpeg/build/ffmpeg/include"])
|
||||
.headerSearchPath("SharedHeaders/ffmpeg/include"),
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
|
|
|||
2
submodules/OpusBinding/Package.swift
vendored
2
submodules/OpusBinding/Package.swift
vendored
|
|
@ -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"])
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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! {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1382,7 +1382,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||
url: "",
|
||||
simple: true,
|
||||
source: .generic,
|
||||
skipTermsOfService: true
|
||||
skipTermsOfService: true,
|
||||
payload: nil
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
197
submodules/TelegramUI/Resources/WebEmbed/HLSVideoPlayer.html
Executable file
197
submodules/TelegramUI/Resources/WebEmbed/HLSVideoPlayer.html
Executable 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>
|
||||
2
submodules/TelegramUI/Resources/WebEmbed/hls.js
Normal file
2
submodules/TelegramUI/Resources/WebEmbed/hls.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,4 +227,11 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
|
|||
|
||||
func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) {
|
||||
}
|
||||
|
||||
func enterNativePictureInPicture() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func exitNativePictureInPicture() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"app": "11.2",
|
||||
"app": "11.2.1",
|
||||
"xcode": "16.0",
|
||||
"bazel": "7.3.1",
|
||||
"macos": "15.0"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue