diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 1a858d3864..d48b69cac1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11127,6 +11127,9 @@ Sorry for the inconvenience."; "Chat.BottomSearchPanel.MessageCount_1" = "message"; "Chat.BottomSearchPanel.MessageCount_any" = "messages"; +"Chat.BottomSearchPanel.StoryCount_1" = "story"; +"Chat.BottomSearchPanel.StoryCount_any" = "stories"; + "Chat.BottomSearchPanel.DisplayModeFormat" = "Show as %@"; "Chat.BottomSearchPanel.DisplayModeChat" = "Chat"; "Chat.BottomSearchPanel.DisplayModeList" = "List"; @@ -11750,9 +11753,11 @@ Sorry for the inconvenience."; "Monetization.TransactionInfo.ViewInExplorer" = "View in Blockchain Explorer"; "Monetization.Intro.Title" = "Earn From Your Channel"; +"Monetization.Intro.Bot.Title" = "Earn From Your Bot"; "Monetization.Intro.Ads.Title" = "Telegram Ads"; "Monetization.Intro.Ads.Text" = "Telegram can display ads in your channel."; +"Monetization.Intro.Bot.Ads.Text" = "Telegram can display ads in your bot."; "Monetization.Intro.Split.Title" = "50:50 Revenue Split"; "Monetization.Intro.Split.Text" = "You receive 50% of the ad revenue in TON."; @@ -11776,13 +11781,18 @@ Sorry for the inconvenience."; "AdsInfo.Info" = "Telegram Ads are very different from ads on other platforms. Ads such as this one:"; "AdsInfo.Respect.Title" = "Respect Your Privacy"; "AdsInfo.Respect.Text" = "Ads on Telegram do not use your personal information and are based on the channel in which you see them."; +"AdsInfo.Bot.Respect.Text" = "Ads on Telegram do not use your personal information and are based on the bot in which you see them."; "AdsInfo.Split.Title" = "Help the Channel Creator"; "AdsInfo.Split.Text" = "50% of the revenue from Telegram Ads goes to the owner of the channel where they are displayed."; +"AdsInfo.Bot.Split.Title" = "Help the Bot Creator"; +"AdsInfo.Bot.Split.Text" = "50% of the revenue from Telegram Ads goes to the owner of the bot where they are displayed."; "AdsInfo.Ads.Title" = "Can Be Removed"; "AdsInfo.Ads.Text" = "You can turn off ads by subscribing to [Telegram Premium](), and Level %@ channels can remove them for their subscribers."; +"AdsInfo.Bot.Ads.Text" = "You can turn off ads by subscribing to [Telegram Premium]()."; "AdsInfo.Launch.Title" = "Can I Launch an Ad?"; "AdsInfo.Launch.Text" = "Anyone can create ads to display in this channel – with minimal budgets. Check out the Telegram Ad Platform for details. [Learn More >]()"; +"AdsInfo.Bot.Launch.Text" = "Anyone can create ads to display in this bot – with minimal budgets. Check out the Telegram Ad Platform for details. [Learn More >]()"; "AdsInfo.Launch.Text_URL" = "https://promote.telegram.org"; "AdsInfo.Understood" = "Understood"; @@ -12393,6 +12403,16 @@ Sorry for the inconvenience."; "HashtagSearch.StoriesFound_any" = "%@ Stories Found"; "HashtagSearch.StoriesFoundInfo" = "View stories with %@"; +"HashtagSearch.Stories_1" = "%@ Story"; +"HashtagSearch.Stories_any" = "%@ Stories"; +"HashtagSearch.LocalStoriesFound" = "%1$@ in %2$@"; + +"HashtagSearch.Posts_1" = "%@ Message"; +"HashtagSearch.Posts_any" = "%@ Messages"; +"HashtagSearch.FoundInfoFormat" = "View %1$@ with %2$@"; +"HashtagSearch.FoundStories" = "stories"; +"HashtagSearch.FoundPosts" = "posts"; + "Stars.BotRevenue.Title" = "Stars Balance"; "Stars.BotRevenue.Revenue.Title" = "Stars Received"; "Stars.BotRevenue.Proceeds.Title" = "Rewards Overview"; @@ -12963,6 +12983,7 @@ Sorry for the inconvenience."; "Gift.View.Title" = "Gift"; "Gift.View.ReceivedTitle" = "Received Gift"; +"Gift.View.UnavailableTitle" = "Unavailable"; "Gift.View.KeepOrConvertDescription" = "You can keep this gift in your Profile or convert it to %@. [More About Stars >]()"; "Gift.View.KeepOrConvertDescription.Stars_1" = "%@ Star"; "Gift.View.KeepOrConvertDescription.Stars_any" = "%@ Stars"; @@ -12972,20 +12993,42 @@ Sorry for the inconvenience."; "Gift.View.OtherDescription" = "%1$@ can keep this gift in their Profile or convert it to %2$@. [More About Stars >]()"; "Gift.View.OtherDescription.Stars_1" = "%@ Star"; "Gift.View.OtherDescription.Stars_any" = "%@ Stars"; +"Gift.View.UnavailableDescription" = "This gift has sold out"; "Gift.View.From" = "From"; "Gift.View.HiddenName" = "Hidden Name"; +"Gift.View.Send" = "send a gift"; "Gift.View.Date" = "Date"; +"Gift.View.FirstSale" = "First Sale"; +"Gift.View.LastSale" = "Last Sale"; +"Gift.View.Value" = "Value"; +"Gift.View.Sale" = "sell for %@"; +"Gift.View.Sale.Stars_1" = "%@ Star"; +"Gift.View.Sale.Stars_any" = "%@ Stars"; "Gift.View.Availability" = "Availability"; "Gift.View.Availability.Of" = "%1$@ of %2$@"; +"Gift.View.Availability.NewOf" = "%1$@ of %2$@ left"; +"Gift.View.Visibility" = "Visibility"; +"Gift.View.Visibility.Visible" = "Visible on your page"; +"Gift.View.Visibility.Hide" = "hide"; "Gift.View.Hide" = "Hide from My Page"; "Gift.View.Display" = "Display on My Page"; "Gift.View.Convert" = "Convert to %@"; "Gift.View.Convert.Stars_1" = "%@ Star"; "Gift.View.Convert.Stars_any" = "%@ Stars"; + +"Gift.View.NameHidden" = "Only you can see the senders's name."; +"Gift.View.NameAndMessageHidden" = "Only you can see the senders's name and message."; + +"Gift.View.DisplayedInfo" = "The gift is visible on your Page. [View >]()"; +"Gift.View.HiddenInfo" = "This gift is hidden. Only you can see it."; + "Gift.Displayed.Title" = "Gift Saved to Profile"; "Gift.Displayed.Text" = "The gift is now displayed in [your profile]()."; +"Gift.Displayed.NewText" = "The gift is now shown on your Page."; +"Gift.Displayed.View" = "View"; "Gift.Hidden.Title" = "Gift Removed from Profile"; "Gift.Hidden.Text" = "The gift is no longer displayed in [your profile]()."; +"Gift.Hidden.NewText" = "The gift is removed from your Page."; "Gift.Convert.Title" = "Convert Gift to Stars"; "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"; @@ -13007,8 +13050,8 @@ Sorry for the inconvenience."; "Gift.Options.Gift.Text" = "Give **%@** gifts that can be kept on the profile or converted to Stars. [What are Stars >]()"; "Gift.Options.Gift.Filter.AllGifts" = "All Gifts"; "Gift.Options.Gift.Filter.Limited" = "Limited"; -"Gift.Options.Gift.Limited" = "Limited"; -"Gift.Options.Gift.SoldOut" = "Sold Out"; +"Gift.Options.Gift.Limited" = "limited"; +"Gift.Options.Gift.SoldOut" = "sold out"; "Gift.Options.SoldOut.Text" = "Sorry, this gift is sold out."; "PeerInfo.PaneGifts" = "Gifts"; @@ -13077,3 +13120,84 @@ Sorry for the inconvenience."; "WebBrowser.AuthChallenge.Title" = "Sign in to %@"; "WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely."; + + +"ChatList.Search.FilterPublicPosts" = "Public Posts"; +"DialogList.SearchSectionPublicPosts" = "Public Posts"; + +"Chat.PrivateMessageEditTimestamp.Date" = "edited %@"; +"Chat.PrivateMessageEditTimestamp.TodayAt" = "edited today at %@"; +"Chat.PrivateMessageEditTimestamp.YesterdayAt" = "edited yesterday at %@"; + +"Stars.Transaction.Gift.Title" = "Gift"; + +"ChatList.Search.Open" = "OPEN"; +"ChatList.Search.ShowMore" = "Show More"; + +"AttachmentMenu.AddPhotoOrVideo" = "Add Photo or Video"; +"AttachmentMenu.AddDocument" = "Add Document"; + +"Chat.BotAd.Title" = "Ad"; +"Chat.BotAd.WhatIsThis" = "what's this?"; + +"ChatList.Search.TopAppsInfo" = "Which apps are included here? [Learn >]()"; + +"TopApps.Info.Title" = "Top Mini Apps"; +"TopApps.Info.Text" = "This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini app in [@botfather]() (as described [here](https://core.telegram.org/bots/webapps#launching-the-main-mini-app)), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on weekly average."; +"TopApps.Info.Done" = "Understood"; + +"Stars.Intro.Transaction.TelegramBotApi.Title" = "Paid Broadcast"; +"Stars.Intro.Transaction.TelegramBotApi.Messages_1" = "%@ Message"; +"Stars.Intro.Transaction.TelegramBotApi.Messages_any" = "%@ Messages"; + +"Stars.Transaction.TelegramBotApi.Title" = "Paid Broadcast"; +"Stars.Transaction.TelegramBotApi.Messages_1" = "%@ Message"; +"Stars.Transaction.TelegramBotApi.Messages_any" = "%@ Messages"; + +"Monetization.Bot.Header" = "Telegram shares 50% of the revenue from ads displayed in your bot. [Learn More >]()"; +"Monetization.Bot.BalanceTitle" = "AVAILABLE BALANCE"; + +"Resolve.ChannelErrorNotFound" = "Sorry, this channel doesn't seem to exist."; + +"Stats.TonBotRevenue.Title" = "TON Balance"; + +"PeerInfo.BotBalance.Title" = "BALANCE"; +"PeerInfo.BotBalance.Ton" = "Ton"; +"PeerInfo.BotBalance.Stars" = "Stars"; + +"Gallery.ToastVideoSpeedSwipe" = "Swipe sideways to adjust speed."; +"Gallery.VideoSettings.QualitySectionTitle" = "QUALITY"; +"Gallery.VideoSettings.SpeedSectionTitle" = "PLAYBACK SPEED"; +"Gallery.VideoSettings.SpeedControlTitle" = "Speed"; + +"Gallery.VideoSettings.QualityAuto" = "Auto"; +"Gallery.VideoSettings.QualityLow" = "Low"; +"Gallery.VideoSettings.QualityMedium" = "Medium"; +"Gallery.VideoSettings.QualityHD" = "High"; +"Gallery.VideoSettings.QualityFHD" = "Full HD"; +"Gallery.VideoSettings.QualityQHD" = "Ultra-High"; + +"Gallery.VideoSettings.IconQualityLow" = "L"; +"Gallery.VideoSettings.IconQualityMedium" = "SD"; +"Gallery.VideoSettings.IconQualityHD" = "HD"; +"Gallery.VideoSettings.IconQualityFHD" = "FHD"; +"Gallery.VideoSettings.IconQualityQHD" = "QHD"; + +"Gallery.MenuSaveToGallery" = "Save to Gallery"; +"Gallery.SaveToGallery.Quality" = "Save in %@p"; +"Gallery.SaveToGallery.Original" = "Save Original"; + +"VideoChat.ScheduleButtonTitle" = "Schedule Video Chat"; + +"Chat.ToastImprovingVideo.Title" = "Improving video..."; +"Chat.ToastImprovingVideo.Text" = "The video will be published after it's optimized for the best viewing experience."; +"Chat.ToastVideoPublished.Title" = "Video Published"; +"Chat.ToastVideoPublished.Action" = "View"; +"Chat.MessageTooltipVideoProcessing" = "Processing video may take a few minutes."; + +"Chat.VideoProcessingServiceMessage_1" = "This video will be published once converted and optimized"; +"Chat.VideoProcessingServiceMessage_any" = "These videos will be published once converted and optimized"; + +"Chat.ScheduledForceSendProcessingVideo.Title" = "Wait!"; +"Chat.ScheduledForceSendProcessingVideo.Text" = "This video hasn't been converted and optimized yet. If you send it now, the viewers of the video may experience slow download speed."; +"Chat.ScheduledForceSendProcessingVideo.Action" = "Send Anyway"; diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index bd2d011b0c..cb67b72a1b 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -106,8 +106,7 @@ class BazelCommandLine: # 1. resolves issues with the linker caused by the swift-objc mixing. # 2. makes the resulting binaries significantly smaller (up to 9% for this project). #'--swiftcopt=-num-threads', '--swiftcopt=1', - '--swiftcopt=-j1', - '--features=swift._num_threads_0_in_swiftcopts', + '--swiftcopt=-num-threads', '--swiftcopt=1', # Strip unsused code. '--features=dead_strip', diff --git a/build-system/bazel-rules/rules_swift b/build-system/bazel-rules/rules_swift index 86dc0f0462..1aec64c218 160000 --- a/build-system/bazel-rules/rules_swift +++ b/build-system/bazel-rules/rules_swift @@ -1 +1 @@ -Subproject commit 86dc0f046269b3001f6f20cec38342c03120a209 +Subproject commit 1aec64c218fc057c2a836e67bd55bc514e0ef8bb diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 95b279d4b1..5a701c043b 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -641,6 +641,7 @@ public enum ChatListSearchFilter: Equatable { case voice case peer(PeerId, Bool, String, String) case date(Int32?, Int32, String) + case publicPosts public var id: Int64 { switch self { @@ -664,6 +665,8 @@ public enum ChatListSearchFilter: Equatable { return 8 case .voice: return 9 + case .publicPosts: + return 10 case let .peer(peerId, _, _, _): return peerId.id._internalGetInt64Value() case let .date(_, date, _): @@ -810,7 +813,7 @@ public enum CollectibleItemInfoScreenSubject { public enum StorySearchControllerScope { - case query(String) + case query(EnginePeer?, String) case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue) } @@ -938,7 +941,7 @@ public protocol SharedAccountContext: AnyObject { func makeStorageManagementController(context: AccountContext) -> ViewController func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? - func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController + func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController @@ -971,7 +974,7 @@ public protocol SharedAccountContext: AnyObject { func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set, messages: [EngineMessage.Id: EngineMessage], peers: [EnginePeer.Id: EnginePeer]) -> Signal func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal func resolveUrlWithProgress(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal - func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) + func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index b2dd59e404..05ab49c8b2 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -63,7 +63,6 @@ public final class ChatMessageItemAssociatedData: Equatable { public let isStandalone: Bool public let isInline: Bool public let showSensitiveContent: Bool - public let starGifts: [Int64: TelegramMediaFile] public init( automaticDownloadPeerType: MediaAutoDownloadPeerType, @@ -97,8 +96,7 @@ public final class ChatMessageItemAssociatedData: Equatable { deviceContactsNumbers: Set = Set(), isStandalone: Bool = false, isInline: Bool = false, - showSensitiveContent: Bool = false, - starGifts: [Int64: TelegramMediaFile] = [:] + showSensitiveContent: Bool = false ) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerId = automaticDownloadPeerId @@ -132,7 +130,6 @@ public final class ChatMessageItemAssociatedData: Equatable { self.isStandalone = isStandalone self.isInline = isInline self.showSensitiveContent = showSensitiveContent - self.starGifts = starGifts } public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { @@ -226,9 +223,6 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.showSensitiveContent != rhs.showSensitiveContent { return false } - if lhs.starGifts != rhs.starGifts { - return false - } return true } } @@ -870,7 +864,7 @@ public struct ChatInputQueryCommandsResult: Equatable { public enum ChatPresentationInputQueryResult: Equatable { case stickers([FoundStickerItem]) - case hashtags([String]) + case hashtags([String], String) case mentions([EnginePeer]) case commands(ChatInputQueryCommandsResult) case emojis([(String, TelegramMediaFile?, String)], NSRange) @@ -884,9 +878,9 @@ public enum ChatPresentationInputQueryResult: Equatable { } else { return false } - case let .hashtags(lhsResults): - if case let .hashtags(rhsResults) = rhs { - return lhsResults == rhsResults + case let .hashtags(lhsResults, lhsQuery): + if case let .hashtags(rhsResults, rhsQuery) = rhs { + return lhsResults == rhsResults && lhsQuery == rhsQuery } else { return false } @@ -1031,14 +1025,18 @@ public protocol ChatController: ViewController { var visibleContextController: ViewController? { get } + var contentContainerNode: ASDisplayNode { get } + var searching: ValuePromise { get } + var searchResultsCount: ValuePromise { get } + var externalSearchResultsCount: Int32? { get set } var alwaysShowSearchResultsAsList: Bool { get set } var includeSavedPeersInSearchResults: Bool { get set } var showListEmptyResults: Bool { get set } + func beginMessageSearch(_ query: String) func updatePresentationMode(_ mode: ChatControllerPresentationMode) - func beginMessageSearch(_ query: String) func displayPromoAnnouncement(text: String) func updatePushedTransition(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) diff --git a/submodules/AccountContext/Sources/IsMediaStreamable.swift b/submodules/AccountContext/Sources/IsMediaStreamable.swift index 5eb69dcf94..34e9a2a029 100644 --- a/submodules/AccountContext/Sources/IsMediaStreamable.swift +++ b/submodules/AccountContext/Sources/IsMediaStreamable.swift @@ -19,7 +19,7 @@ public func isMediaStreamable(message: Message, media: TelegramMediaFile) -> Boo } for attribute in media.attributes { if case let .Video(_, _, flags, _, _, _) = attribute { - if flags.contains(.supportsStreaming) { + if flags.contains(.supportsStreaming) || !media.alternativeRepresentations.isEmpty { return true } break diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index 8ff0bf498f..80273b5756 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -37,6 +37,7 @@ public protocol UniversalVideoContentNode: AnyObject { func setBaseRate(_ baseRate: Double) func setVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int func removePlaybackCompleted(_ index: Int) func fetchControl(_ control: UniversalVideoNodeFetchControl) @@ -44,6 +45,7 @@ public protocol UniversalVideoContentNode: AnyObject { func setCanPlaybackWithoutHierarchy(_ canPlaybackWithoutHierarchy: Bool) func enterNativePictureInPicture() -> Bool func exitNativePictureInPicture() + func setNativePictureInPictureIsActive(_ value: Bool) } public protocol UniversalVideoContent { @@ -366,6 +368,16 @@ public final class UniversalVideoNode: ASDisplayNode { return result } + public func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + var result: Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError>? + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode { + result = contentNode.videoQualityStateSignal() + } + }) + return result ?? .single(nil) + } + public func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { @@ -445,4 +457,12 @@ public final class UniversalVideoNode: ASDisplayNode { } }) } + + public func setNativePictureInPictureIsActive(_ value: Bool) { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.setNativePictureInPictureIsActive(value) + } + }) + } } diff --git a/submodules/AlertUI/Sources/ThemedTextAlertController.swift b/submodules/AlertUI/Sources/ThemedTextAlertController.swift index c7ef5ddff0..d2d40d0464 100644 --- a/submodules/AlertUI/Sources/ThemedTextAlertController.swift +++ b/submodules/AlertUI/Sources/ThemedTextAlertController.swift @@ -14,8 +14,8 @@ public final class AlertControllerContext { } } -public func textAlertController(alertContext: AlertControllerContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true) -> AlertController { - let controller = standardTextAlertController(theme: alertContext.theme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap) +public func textAlertController(alertContext: AlertControllerContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { + let controller = standardTextAlertController(theme: alertContext.theme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction) let presentationDataDisposable = alertContext.themeSignal.start(next: { [weak controller] theme in controller?.theme = theme }) diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift index 5675dd5a46..5528cec2cb 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -352,7 +352,8 @@ final class BrowserAddressListComponent: Component { highlighting: .default, updateIsHighlighted: { view, _ in - }) + } + ) ), environment: {}, containerSize: itemFrame.size diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index 273bd0a91f..9515edaf6a 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -148,6 +148,8 @@ public final class BrowserBookmarksScreen: ViewController { }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 5932fd64de..d67548da26 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -20,7 +20,7 @@ import UrlEscaping final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData - let file: TelegramMediaFile + let file: FileMediaReference private let webView: WKWebView @@ -47,7 +47,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate private var tempFile: TempBoxFile? - init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + init(context: AccountContext, presentationData: PresentationData, file: FileMediaReference) { self.context = context self.uuid = UUID() self.presentationData = presentationData @@ -63,9 +63,9 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate var title: String = "file" var url = "" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.media.resource) { var updatedPath = path - if let fileName = file.fileName { + if let fileName = file.media.fileName { let tempFile = TempBox.shared.file(path: path, fileName: fileName) updatedPath = tempFile.path self.tempFile = tempFile @@ -73,8 +73,13 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate url = updatedPath } - let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) - self.webView.load(request) + let updatedUrl = URL(fileURLWithPath: updatedPath) + let request = URLRequest(url: updatedUrl) + if updatedPath.lowercased().hasSuffix(".txt"), let data = try? Data(contentsOf: updatedUrl) { + self.webView.load(data, mimeType: "text/plain", characterEncodingName: "UTF-8", baseURL: URL(string: "http://localhost")!) + } else { + self.webView.load(request) + } } self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document) diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 977dcceafe..a9cb596ac5 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -104,6 +104,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.preloadedResouces = preloadedResouces self.originalContent = originalContent self.url = url + self.initialAnchor = anchor self.uuid = UUID() @@ -268,10 +269,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg private func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) { if self.webPage != webPage { if self.webPage != nil && self.currentLayout != nil { - if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { - self.scrollNode.view.superview?.insertSubview(snaphotView, aboveSubview: self.scrollNode.view) - snaphotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snaphotView] _ in - snaphotView?.removeFromSuperview() + if let snapshotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.scrollNode.frame + self.scrollNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.scrollNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() }) } } @@ -403,7 +405,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg var scrollInsets = insets scrollInsets.top = 0.0 - if self.scrollNode.view.contentInset != insets { + if self.scrollNode.view.contentInset != scrollInsets { self.scrollNode.view.contentInset = scrollInsets self.scrollNode.view.scrollIndicatorInsets = scrollInsets } @@ -963,7 +965,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg default: strongSelf.loadProgress.set(1.0) strongSelf.minimize() - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in switch navigation { case let .chat(_, subject, peekData): if let navigationController = strongSelf.getNavigationController() { diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 2b1aec9f0f..d6d6742793 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -20,7 +20,7 @@ import PDFKit final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDFDocumentDelegate { private let context: AccountContext private var presentationData: PresentationData - let file: TelegramMediaFile + let file: FileMediaReference private let pdfView: PDFView private let scrollView: UIScrollView! @@ -53,7 +53,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF private var tempFile: TempBoxFile? - init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + init(context: AccountContext, presentationData: PresentationData, file: FileMediaReference) { self.context = context self.uuid = UUID() self.presentationData = presentationData @@ -86,9 +86,9 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF var title = "file" var url = "" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.media.resource) { var updatedPath = path - if let fileName = file.fileName { + if let fileName = file.media.fileName { let tempFile = TempBox.shared.file(path: path, fileName: fileName) updatedPath = tempFile.path self.tempFile = tempFile diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index a6023a90d4..025c2fd086 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -569,10 +569,10 @@ public class BrowserScreen: ViewController, MinimizableController { var isDocument = false if let content = self.content.last { if let documentContent = content as? BrowserDocumentContent { - subject = .media(.standalone(media: documentContent.file)) + subject = .media(documentContent.file.abstract) isDocument = true } else if let documentContent = content as? BrowserPdfContent { - subject = .media(.standalone(media: documentContent.file)) + subject = .media(documentContent.file.abstract) isDocument = true } else { subject = .url(url) @@ -650,7 +650,7 @@ public class BrowserScreen: ViewController, MinimizableController { switch controller.subject { case let .document(file, canShare), let .pdfDocument(file, canShare): processed = true - controller.openDocument(file, canShare) + controller.openDocument(file.media, canShare) default: break } @@ -1486,13 +1486,13 @@ public class BrowserScreen: ViewController, MinimizableController { public enum Subject { case webPage(url: String) case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation, preloadedResources: [Any]?) - case document(file: TelegramMediaFile, canShare: Bool) - case pdfDocument(file: TelegramMediaFile, canShare: Bool) + case document(file: FileMediaReference, canShare: Bool) + case pdfDocument(file: FileMediaReference, canShare: Bool) public var fileId: MediaId? { switch self { case let .document(file, _), let .pdfDocument(file, _): - return file.fileId + return file.media.fileId default: return nil } diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 9835fdfe84..9fa6fab203 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1413,20 +1413,22 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } let js = """ - var favicons = []; - var nodeList = document.getElementsByTagName('link'); - for (var i = 0; i < nodeList.length; i++) - { - if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon'))) + (function() { + var favicons = []; + var nodeList = document.getElementsByTagName('link'); + for (var i = 0; i < nodeList.length; i++) { - const node = nodeList[i]; - favicons.push({ - url: node.getAttribute('href'), - sizes: node.getAttribute('sizes') - }); + if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon'))) + { + const node = nodeList[i]; + favicons.push({ + url: node.getAttribute('href'), + sizes: node.getAttribute('sizes') + }); + } } - } - favicons; + return favicons; + })(); """ self.webView.evaluateJavaScript(js, completionHandler: { [weak self] jsResult, _ in guard let self, let favicons = jsResult as? [Any] else { @@ -1706,11 +1708,10 @@ let setupFontFunctions = """ """ private let videoSource = """ +document.addEventListener('DOMContentLoaded', () => { function tgBrowserDisableWebkitEnterFullscreen(videoElement) { if (videoElement && videoElement.webkitEnterFullscreen) { - Object.defineProperty(videoElement, 'webkitEnterFullscreen', { - value: undefined - }); + videoElement.setAttribute('playsinline', ''); } } @@ -1745,6 +1746,7 @@ _tgbrowser_observer.observe(document.body, { function tgBrowserDisconnectObserver() { _tgbrowser_observer.disconnect(); } +}); """ let setupTouchObservers = diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index 937fb4f4d0..6179e2bf82 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -443,6 +443,7 @@ final class CallListControllerNode: ASDisplayNode { } let previousView = Atomic(value: nil) + let previousType = Atomic(value: nil) let showSettings: Bool switch mode { @@ -505,7 +506,8 @@ final class CallListControllerNode: ASDisplayNode { let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(view: update.view, groupCalls: groupCalls, state: state, showSettings: showSettings, showCallsTab: showCallsTab, isRecentCalls: type == .all, currentGroupCallPeerId: currentGroupCallPeerId), presentationData: state.presentationData) let previous = previousView.swap(processedView) - + let previousType = previousType.swap(type) + let reason: CallListNodeViewTransitionReason var prepareOnMainQueue = false @@ -565,8 +567,12 @@ final class CallListControllerNode: ASDisplayNode { } } } + var scrollPosition = update.scrollPosition + if previousType != type { + scrollPosition = .top(animated: false) + } - return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, disableAnimations: disableAnimations, context: context, scrollPosition: update.scrollPosition) + return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, disableAnimations: disableAnimations, context: context, scrollPosition: scrollPosition) |> map({ mappedCallListNodeViewListTransition(context: context, presentationData: state.presentationData, showSettings: showSettings, nodeInteraction: nodeInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) } diff --git a/submodules/CallListUI/Sources/CallListViewTransition.swift b/submodules/CallListUI/Sources/CallListViewTransition.swift index 27c8725318..3925952182 100644 --- a/submodules/CallListUI/Sources/CallListViewTransition.swift +++ b/submodules/CallListUI/Sources/CallListViewTransition.swift @@ -45,6 +45,7 @@ struct CallListNodeViewTransition { } enum CallListNodeViewScrollPosition { + case top(animated: Bool) case index(index: EngineMessage.Index, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) } @@ -135,6 +136,8 @@ func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toV if let scrollPosition = scrollPosition { switch scrollPosition { + case let .top(animated): + scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: animated, curve: .Default(duration: nil), directionHint: .Up) case let .index(scrollIndex, position, directionHint, animated): var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { diff --git a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift index 0fbfa73e98..3a51304be1 100644 --- a/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift +++ b/submodules/ChatListSearchItemHeader/Sources/ChatListSearchItemHeader.swift @@ -31,6 +31,7 @@ public enum ChatListSearchItemHeaderType { case downloading case recentDownloads case topics + case publicPosts case text(String, AnyHashable) fileprivate func title(strings: PresentationStrings) -> String { @@ -91,6 +92,8 @@ public enum ChatListSearchItemHeaderType { return strings.DownloadList_DownloadedHeader case .topics: return strings.DialogList_SearchSectionTopics + case .publicPosts: + return strings.DialogList_SearchSectionPublicPosts case let .text(text, _): return text } @@ -154,6 +157,8 @@ public enum ChatListSearchItemHeaderType { return .recentDownloads case .topics: return .topics + case .publicPosts: + return .publicPosts case let .text(_, id): return .text(id) } @@ -192,6 +197,7 @@ private enum ChatListSearchItemHeaderId: Hashable { case downloading case recentDownloads case topics + case publicPosts case text(AnyHashable) } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index b710deca99..3a93d96776 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -253,6 +253,13 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in var updatedItems: [ContextMenuItem] = [] + updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) + }, iconPosition: .left, action: { c, _ in + c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + }))) + updatedItems.append(.separator) + for filter in filters { if case let .filter(_, title, _, data) = filter { let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId) @@ -338,16 +345,10 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch }))) } } - - updatedItems.append(.separator) - updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) - }, iconPosition: .left, action: { c, _ in - c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) - }))) - + c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true) }))) + items.append(.separator) } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index b5168b166d..f0c25a3f16 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -60,8 +60,9 @@ final class ChatListSearchInteraction { let dismissInput: () -> Void let getSelectedMessageIds: () -> Set? let openStories: ((PeerId, ASDisplayNode) -> Void)? + let switchToFilter: (ChatListSearchPaneKey) -> Void - init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set?, openStories: ((PeerId, ASDisplayNode) -> Void)?) { + init(openPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, openMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set?, openStories: ((PeerId, ASDisplayNode) -> Void)?, switchToFilter: @escaping (ChatListSearchPaneKey) -> Void) { self.openPeer = openPeer self.openDisabledPeer = openDisabledPeer self.openMessage = openMessage @@ -76,6 +77,7 @@ final class ChatListSearchInteraction { self.dismissInput = dismissInput self.getSelectedMessageIds = getSelectedMessageIds self.openStories = openStories + self.switchToFilter = switchToFilter } } @@ -120,6 +122,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var suggestedFilters: [ChatListSearchFilter]? private let suggestedFiltersDisposable = MetaDisposable() private var forumPeer: EnginePeer? + private var hasPublicPostsTab = false + private var showPublicPostsTab = false private var shareStatusDisposable: MetaDisposable? @@ -199,7 +203,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let _ = openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in present(c, nil) }, openResolved: { [weak self] resolved in - context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peerId, navigation in + context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peerId, navigation in }, sendFile: nil, @@ -281,53 +285,27 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo avatarNode: sourceNode as? AvatarNode, sharedProgressDisposable: self.sharedOpenStoryDisposable ) + }, switchToFilter: { [weak self] filter in + guard let self else { + return + } + if filter == .publicPosts && !self.showPublicPostsTab { + self.showPublicPostsTab = true + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + Queue.mainQueue().justDispatch { + self.paneContainerNode.requestSelectPane(filter) + } }) self.paneContainerNode.interaction = interaction self.paneContainerNode.currentPaneUpdated = { [weak self] key, transitionFraction, transition in - if let strongSelf = self, let key = key { - var filterKey: ChatListSearchFilter - switch key { - case .chats: - filterKey = .chats - case .topics: - filterKey = .topics - case .channels: - filterKey = .channels - case .apps: - filterKey = .apps - case .media: - filterKey = .media - case .downloads: - filterKey = .downloads - case .links: - filterKey = .links - case .files: - filterKey = .files - case .music: - filterKey = .music - case .voice: - filterKey = .voice - } - strongSelf.selectedFilter = .filter(filterKey) - strongSelf.selectedFilterPromise.set(.single(strongSelf.selectedFilter)) - strongSelf.transitionFraction = transitionFraction - - if let (layout, _) = strongSelf.validLayout { - let filters: [ChatListSearchFilter] - if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty { - filters = suggestedFilters - } else { - var isForum = false - if case .forum = strongSelf.location { - isForum = true - } - - filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && strongSelf.hasDownloads).map(\.filter) - } - strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition) - } + guard let self, let key else { + return } + self.currentPaneUpdated(key, transitionFraction: transitionFraction, transition: transition) } self.paneContainerNode.requesDismissInput = { @@ -368,6 +346,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo key = .music case .voice: key = .voice + case .publicPosts: + key = .publicPosts case let .date(minDate, maxDate, title): date = (minDate, maxDate, title) case let .peer(id, isGroup, _, compactDisplayTitle): @@ -435,7 +415,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return (.complete() |> delay(0.25, queue: Queue.mainQueue())) |> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))) } - } |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> [ChatListSearchFilter] in + } |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> ([ChatListSearchFilter], Bool) in var suggestedFilters: [ChatListSearchFilter] = [] if !dates.isEmpty { let formatter = DateFormatter() @@ -481,26 +461,34 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo existingPeerIds.insert(peer.id) } } - return suggestedFilters + return (suggestedFilters, searchQuery?.hasPrefix("#") ?? false) } - |> deliverOnMainQueue).startStrict(next: { [weak self] filters in + |> deliverOnMainQueue).startStrict(next: { [weak self] filters, hasPublicPosts in guard let strongSelf = self else { return } var filteredFilters: [ChatListSearchFilter] = [] - for filter in filters { - if case .date = filter, strongSelf.searchOptionsValue?.date == nil { - filteredFilters.append(filter) - } - if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil { - filteredFilters.append(filter) + if !hasPublicPosts { + for filter in filters { + if case .date = filter, strongSelf.searchOptionsValue?.date == nil { + filteredFilters.append(filter) + } + if case .peer = filter, strongSelf.searchOptionsValue?.peer == nil { + filteredFilters.append(filter) + } } } let previousFilters = strongSelf.suggestedFilters strongSelf.suggestedFilters = filteredFilters - if filteredFilters != previousFilters { + let previousHasPublicPosts = strongSelf.hasPublicPostsTab + strongSelf.hasPublicPostsTab = hasPublicPosts + if !hasPublicPosts { + strongSelf.showPublicPostsTab = false + } + + if filteredFilters != previousFilters || hasPublicPosts != previousHasPublicPosts { if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } @@ -646,12 +634,63 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo override public func searchTextUpdated(text: String) { let searchQuery: String? = !text.isEmpty ? text : nil + + if !text.hasPrefix("#") && self.paneContainerNode.currentPaneKey == .publicPosts { + self.paneContainerNode.requestSelectPane(.chats) + } + self.searchQuery.set(.single(searchQuery)) self.searchQueryValue = searchQuery self.suggestedDates.set(.single(suggestDates(for: text, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat))) } + private func currentPaneUpdated(_ key: ChatListSearchPaneKey, transitionFraction: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { + var filterKey: ChatListSearchFilter + switch key { + case .chats: + filterKey = .chats + case .topics: + filterKey = .topics + case .channels: + filterKey = .channels + case .apps: + filterKey = .apps + case .media: + filterKey = .media + case .downloads: + filterKey = .downloads + case .links: + filterKey = .links + case .files: + filterKey = .files + case .music: + filterKey = .music + case .voice: + filterKey = .voice + case .publicPosts: + filterKey = .publicPosts + } + self.selectedFilter = .filter(filterKey) + self.selectedFilterPromise.set(.single(self.selectedFilter)) + self.transitionFraction = transitionFraction + + if let (layout, _) = self.validLayout { + let filters: [ChatListSearchFilter] + if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { + filters = suggestedFilters + } else { + var isForum = false + if case .forum = self.location { + isForum = true + } + + filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter) + } + self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition) + } + } + public func search(filter: ChatListSearchFilter, query: String?) { let key: ChatListSearchPaneKey switch filter { @@ -713,7 +752,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty { filters = suggestedFilters } else { - filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads).map(\.filter) + filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter) } let overflowInset: CGFloat = 20.0 @@ -891,7 +930,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let availablePanes: [ChatListSearchPaneKey] if self.displaySearchFilters { - availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads) + availablePanes = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: self.hasDownloads, hasPublicPosts: self.hasPublicPostsTab) } else { availablePanes = isForum ? [.topics] : [.chats] } diff --git a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift index 127b853313..fb1a9bd468 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchFiltersContainerNode.swift @@ -108,6 +108,9 @@ private final class ItemNode: ASDisplayNode { case .voice: title = presentationData.strings.ChatList_Search_FilterVoice icon = nil + case .publicPosts: + title = presentationData.strings.ChatList_Search_FilterPublicPosts + icon = nil case let .peer(peerId, isGroup, displayTitle, _): title = displayTitle let image: UIImage? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 995dcb2fe9..d5c691cc9a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -35,6 +35,7 @@ import AvatarNode private enum ChatListRecentEntryStableId: Hashable { case topPeers case peerId(EnginePeer.Id, ChatListRecentEntry.Section) + case footer } private enum ChatListRecentEntry: Comparable, Identifiable { @@ -46,13 +47,16 @@ private enum ChatListRecentEntry: Comparable, Identifiable { case topPeers([EnginePeer], PresentationTheme, PresentationStrings) case peer(index: Int, peer: RecentlySearchedPeer, Section, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, EngineGlobalNotificationSettings, PeerStoryStats?, Bool) + case footer(PresentationTheme, String) var stableId: ChatListRecentEntryStableId { switch self { - case .topPeers: - return .topPeers - case let .peer(_, peer, section, _, _, _, _, _, _, _, _): - return .peerId(peer.peer.peerId, section) + case .topPeers: + return .topPeers + case let .peer(_, peer, section, _, _, _, _, _, _, _, _): + return .peerId(peer.peer.peerId, section) + case .footer: + return .footer } } @@ -79,6 +83,12 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } else { return false } + case let .footer(lhsTheme, lhsText): + if case let .footer(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } @@ -88,11 +98,15 @@ private enum ChatListRecentEntry: Comparable, Identifiable { return true case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _): switch rhs { - case .topPeers: - return false + case .topPeers: + return false case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _): - return lhsIndex <= rhsIndex + return lhsIndex <= rhsIndex + case .footer: + return true } + case .footer: + return false } } @@ -110,7 +124,8 @@ private enum ChatListRecentEntry: Comparable, Identifiable { animationRenderer: MultiAnimationRenderer, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, isChannelsTabExpanded: Bool?, - toggleChannelsTabExpanded: @escaping () -> Void + toggleChannelsTabExpanded: @escaping () -> Void, + openTopAppsInfo: @escaping () -> Void ) -> ListViewItem { switch self { case let .topPeers(peers, theme, strings): @@ -271,6 +286,16 @@ private enum ChatListRecentEntry: Comparable, Identifiable { }) } + var buttonAction: ContactsPeerItemButtonAction? + if case .chats = key, case let .user(user) = primaryPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + buttonAction = ContactsPeerItemButtonAction( + title: presentationData.strings.ChatList_Search_Open, + action: { peer, _, _ in + peerSelected(primaryPeer, nil, true) + } + ) + } + return ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), sortOrder: nameSortOrder, @@ -284,8 +309,10 @@ private enum ChatListRecentEntry: Comparable, Identifiable { enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + buttonAction: buttonAction, index: nil, header: header, + alwaysShowLastSeparator: key == .apps, action: { _ in if let chatPeer = peer.peer.peers[peer.peer.peerId] { peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels || section == .popularApps) @@ -332,6 +359,10 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } } ) + case let .footer(_, text): + return ItemListTextItem(presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), text: .markdown(text), sectionId: 0, linkAction: { _ in + openTopAppsInfo() + }, style: .plain, textSize: .larger, textAlignment: .center, trimBottomInset: true, additionalInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0)) } } } @@ -394,6 +425,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case generic case downloading case recentlyDownloaded + case publicPosts } case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) @@ -565,7 +597,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } - public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ListViewItem { + public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void) -> ListViewItem { switch self { case let .topic(peer, threadInfo, _, theme, strings, expandType): let actionTitle: String? @@ -876,7 +908,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openStories(peer.id, sourceNode.avatarNode) } }) - case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused, storyStats, requiresPremiumForMessaging): + case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, section, allPaused, storyStats, requiresPremiumForMessaging): let header: ChatListSearchItemHeader switch orderingKey { case .downloading: @@ -894,11 +926,21 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openClearRecentlyDownloaded() }) case .index: - var headerType: ChatListSearchItemHeaderType = .messages(location: nil) - if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { - headerType = .messages(location: peer.compactDisplayTitle) + if case .publicPosts = section { + if case .publicPosts = key { + header = ChatListSearchItemHeader(type: .publicPosts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } else { + header = ChatListSearchItemHeader(type: .publicPosts, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "\(presentationData.strings.ChatList_Search_ShowMore) >", action: { + openPublicPosts() + }) + } + } else { + var headerType: ChatListSearchItemHeaderType = .messages(location: nil) + if case let .forum(peerId) = location, let peer = peer.peer, peer.id == peerId { + headerType = .messages(location: peer.compactDisplayTitle) + } + header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } - header = ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) } let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none var isMedia = false @@ -1021,6 +1063,7 @@ private func chatListSearchContainerPreparedRecentTransition( animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, + openTopAppsInfo: @escaping () -> Void, isChannelsTabExpanded: Bool?, toggleChannelsTabExpanded: @escaping () -> Void, isEmpty: Bool @@ -1028,18 +1071,18 @@ private func chatListSearchContainerPreparedRecentTransition( let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdateAll) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded, openTopAppsInfo: openTopAppsInfo), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, filter: filter, key: key, peerSelected: peerSelected, disabledPeerSelected: disabledPeerSelected, peerContextAction: peerContextAction, clearRecentlySearchedPeers: clearRecentlySearchedPeers, deletePeer: deletePeer, animationCache: animationCache, animationRenderer: animationRenderer, openStories: openStories, isChannelsTabExpanded: isChannelsTabExpanded, toggleChannelsTabExpanded: toggleChannelsTabExpanded, openTopAppsInfo: openTopAppsInfo), directionHint: nil) } return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: isEmpty) } -public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void) -> ChatListSearchContainerTransition { +public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int64, isFirstInList: Bool)?) -> Void)?, openClearRecentlyDownloaded: @escaping () -> Void, toggleAllPaused: @escaping () -> Void, openStories: @escaping (EnginePeer.Id, AvatarNode) -> Void, openPublicPosts: @escaping () -> Void) -> ChatListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, requestPeerType: requestPeerType, location: location, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openClearRecentlyDownloaded: openClearRecentlyDownloaded, toggleAllPaused: toggleAllPaused, openStories: openStories, openPublicPosts: openPublicPosts), directionHint: nil) } return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated) } @@ -1297,6 +1340,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var searchQueryValue: String? private var searchOptionsValue: ChatListSearchOptions? + var isCurrent: Bool = false + private let _isSearching = ValuePromise(false, ignoreRepeated: true) public var isSearching: Signal { return self._isSearching.get() @@ -1373,6 +1418,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { tagMask = nil case .topics: tagMask = nil + case .publicPosts: + tagMask = nil case .channels: tagMask = nil case .apps: @@ -1748,114 +1795,118 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundLocalPeers = .single(([], [:], Set())) } } else if let query = query, (key == .chats || key == .topics) { - let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() - |> map { peers -> [RecentlySearchedPeer] in - let allIds = peers.map(\.peer.peerId) - - let updatedState = previousRecentlySearchedPeersState.modify { current in - if var current = current, current.query == query { - current.ids = current.ids.filter { id in - allIds.contains(id) - } - - return current - } else { - var state = SearchedPeersState() - state.ids = allIds - state.query = query - return state - } - } - - var result: [RecentlySearchedPeer] = [] - if let updatedState = updatedState { - for id in updatedState.ids { - for peer in peers { - if id == peer.peer.peerId { - result.append(peer) + if query.hasPrefix("#") { + foundLocalPeers = .single(([], [:], Set())) + } else { + let fixedOrRemovedRecentlySearchedPeers = context.engine.peers.recentlySearchedPeers() + |> map { peers -> [RecentlySearchedPeer] in + let allIds = peers.map(\.peer.peerId) + + let updatedState = previousRecentlySearchedPeersState.modify { current in + if var current = current, current.query == query { + current.ids = current.ids.filter { id in + allIds.contains(id) } + + return current + } else { + var state = SearchedPeersState() + state.ids = allIds + state.query = query + return state } } - } - - return result - } - - foundLocalPeers = combineLatest( - context.engine.contacts.searchLocalPeers(query: query.lowercased()), - fixedOrRemovedRecentlySearchedPeers - ) - |> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional], [EnginePeer.Id: Int], [EngineRenderedPeer], Set, EngineGlobalNotificationSettings), NoError> in - let recentlySearched = allRecentlySearched.filter { peer in - guard let peer = peer.peer.peer else { - return false - } - return peer.indexName.matchesByTokens(query) - } - - var peerIds = Set() - - var peers: [EngineRenderedPeer] = [] - for peer in recentlySearched { - if !peerIds.contains(peer.peer.peerId) { - peerIds.insert(peer.peer.peerId) - peers.append(EngineRenderedPeer(peer.peer)) - } - } - for peer in local { - if !peerIds.contains(peer.peerId) { - peerIds.insert(peer.peerId) - peers.append(peer) - } - } - - return context.engine.data.subscribe( - EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in - return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) - } - ), - EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in - return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) - } - ), - TelegramEngine.EngineData.Item.NotificationSettings.Global() - ) - |> map { notificationSettings, unreadCounts, globalNotificationSettings in - return (notificationSettings, unreadCounts, peers, Set(recentlySearched.map(\.peer.peerId)), globalNotificationSettings) - } - } - |> map { notificationSettings, unreadCounts, peers, recentlySearchedPeerIds, globalNotificationSettings -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in - var unread: [EnginePeer.Id: (Int32, Bool)] = [:] - for peer in peers { - var isMuted = false - if let peerNotificationSettings = notificationSettings[peer.peerId], let peerNotificationSettings { - if case let .muted(until) = peerNotificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { - isMuted = true - } else if case .default = peerNotificationSettings.muteState { - if let peer = peer.peer { - if case .user = peer { - isMuted = !globalNotificationSettings.privateChats.enabled - } else if case .legacyGroup = peer { - isMuted = !globalNotificationSettings.groupChats.enabled - } else if case let .channel(channel) = peer { - switch channel.info { - case .group: - isMuted = !globalNotificationSettings.groupChats.enabled - case .broadcast: - isMuted = !globalNotificationSettings.channels.enabled - } + + var result: [RecentlySearchedPeer] = [] + if let updatedState = updatedState { + for id in updatedState.ids { + for peer in peers { + if id == peer.peer.peerId { + result.append(peer) } } } } - let unreadCount = unreadCounts[peer.peerId] - if let unreadCount = unreadCount, unreadCount > 0 { - unread[peer.peerId] = (Int32(unreadCount), isMuted) + + return result + } + + foundLocalPeers = combineLatest( + context.engine.contacts.searchLocalPeers(query: query.lowercased()), + fixedOrRemovedRecentlySearchedPeers + ) + |> mapToSignal { local, allRecentlySearched -> Signal<([EnginePeer.Id: Optional], [EnginePeer.Id: Int], [EngineRenderedPeer], Set, EngineGlobalNotificationSettings), NoError> in + let recentlySearched = allRecentlySearched.filter { peer in + guard let peer = peer.peer.peer else { + return false + } + return peer.indexName.matchesByTokens(query) + } + + var peerIds = Set() + + var peers: [EngineRenderedPeer] = [] + for peer in recentlySearched { + if !peerIds.contains(peer.peer.peerId) { + peerIds.insert(peer.peer.peerId) + peers.append(EngineRenderedPeer(peer.peer)) + } + } + for peer in local { + if !peerIds.contains(peer.peerId) { + peerIds.insert(peer.peerId) + peers.append(peer) + } + } + + return context.engine.data.subscribe( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.NotificationSettings in + return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId) + } + ), + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Messages.PeerUnreadCount in + return TelegramEngine.EngineData.Item.Messages.PeerUnreadCount(id: peerId) + } + ), + TelegramEngine.EngineData.Item.NotificationSettings.Global() + ) + |> map { notificationSettings, unreadCounts, globalNotificationSettings in + return (notificationSettings, unreadCounts, peers, Set(recentlySearched.map(\.peer.peerId)), globalNotificationSettings) } } - return (peers: peers, unread: unread, recentlySearchedPeerIds: recentlySearchedPeerIds) + |> map { notificationSettings, unreadCounts, peers, recentlySearchedPeerIds, globalNotificationSettings -> (peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set) in + var unread: [EnginePeer.Id: (Int32, Bool)] = [:] + for peer in peers { + var isMuted = false + if let peerNotificationSettings = notificationSettings[peer.peerId], let peerNotificationSettings { + if case let .muted(until) = peerNotificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + isMuted = true + } else if case .default = peerNotificationSettings.muteState { + if let peer = peer.peer { + if case .user = peer { + isMuted = !globalNotificationSettings.privateChats.enabled + } else if case .legacyGroup = peer { + isMuted = !globalNotificationSettings.groupChats.enabled + } else if case let .channel(channel) = peer { + switch channel.info { + case .group: + isMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + isMuted = !globalNotificationSettings.channels.enabled + } + } + } + } + } + let unreadCount = unreadCounts[peer.peerId] + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peer.peerId] = (Int32(unreadCount), isMuted) + } + } + return (peers: peers, unread: unread, recentlySearchedPeerIds: recentlySearchedPeerIds) + } } } else if let query = query, key == .channels { foundLocalPeers = combineLatest( @@ -2068,13 +2119,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case .savedMessagesChats = location { foundRemotePeers = .single(([], [], false)) } else if let query = query, case .chats = key { - foundRemotePeers = ( - .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) - |> then( - globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) - |> map { ($0.0, $0.1, false) } + if query.hasPrefix("#") { + foundRemotePeers = .single(([], [], false)) + } else { + foundRemotePeers = ( + .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) + |> then( + globalPeerSearchContext.searchRemotePeers(engine: context.engine, query: query) + |> map { ($0.0, $0.1, false) } + ) ) - ) + } } else if let query = query, case .channels = key { foundRemotePeers = ( .single((currentRemotePeersValue.0, currentRemotePeersValue.1, true)) @@ -2133,8 +2188,75 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } + let foundPublicMessages: Signal<([FoundRemoteMessages], Bool), NoError> + if key == .chats || key == .publicPosts, let query, query.hasPrefix("#") { + let searchSignal = context.engine.messages.searchHashtagPosts(hashtag: finalQuery, state: nil, limit: 10) + + let loadMore: Signal<([FoundRemoteMessages], Bool), NoError> + if key == .publicPosts { + loadMore = searchContexts.get() + |> mapToSignal { searchContexts -> Signal<([FoundRemoteMessages], Bool), NoError> in + let i = 0 + if let searchContext = searchContexts[i], searchContext.result.hasMore { + if let _ = searchContext.loadMoreIndex { + return context.engine.messages.searchHashtagPosts(hashtag: finalQuery, state: searchContext.result.state, limit: 80) + |> map { result, updatedState -> ChatListSearchMessagesResult in + return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState) + } + |> mapToSignal { foundMessages -> Signal<([FoundRemoteMessages], Bool), NoError> in + updateSearchContexts { previous in + let updated = ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil) + var previous = previous + previous[i] = updated + return (previous, true) + } + return .complete() + } + } else { + var currentResults: [FoundRemoteMessages] = [] + if let currentContext = searchContexts[i] { + currentResults.append(FoundRemoteMessages(messages: currentContext.result.messages, readCounters: currentContext.result.readStates, threadsData: currentContext.result.threadInfo, totalCount: currentContext.result.totalCount)) + } + return .single((currentResults, false)) + } + } + + return .complete() + } + } else { + loadMore = .complete() + } + + foundPublicMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], true)) + |> then( + searchSignal + |> map { result -> ([FoundRemoteMessages], Bool) in + updateSearchContexts { _ in + var resultContexts: [Int: ChatListSearchMessagesContext] = [:] + resultContexts[0] = ChatListSearchMessagesContext(result: ChatListSearchMessagesResult(query: finalQuery, messages: result.0.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.0.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.0.threadInfo, hasMore: !result.0.completed, totalCount: result.0.totalCount, state: result.1), loadMoreIndex: nil) + return (resultContexts, true) + } + + let foundMessages = result.0 + let messages: [EngineMessage] + if key == .chats { + messages = foundMessages.messages.prefix(3).map { EngineMessage($0) } + } else { + messages = foundMessages.messages.map { EngineMessage($0) } + } + return ([FoundRemoteMessages(messages: messages, readCounters: foundMessages.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadsData: foundMessages.threadInfo, totalCount: foundMessages.totalCount)], false) + } + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + |> then(loadMore) + ) + } else { + foundPublicMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } + let foundRemoteMessages: Signal<([FoundRemoteMessages], Bool), NoError> - if case .savedMessagesChats = location { + if key == .publicPosts { + foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) + } else if case .savedMessagesChats = location { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) } else if peersFilter.contains(.doNotSearchMessages) { foundRemoteMessages = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) @@ -2146,13 +2268,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] = searchLocations.map { searchLocation in - let limit: Int32 - #if DEBUG - limit = 50 - #else - limit = 50 - #endif - return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: limit) + return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50) } let searchSignal = combineLatest(searchSignals) @@ -2294,9 +2410,21 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { foundThreads = .single([]) } - return combineLatest(accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationDataPromise.get(), searchStatePromise.get(), selectionPromise.get(), resolvedMessage, fixedRecentlySearchedPeers, foundThreads) - |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in - let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 + return combineLatest( + accountPeer, + foundLocalPeers, + foundRemotePeers, + foundRemoteMessages, + foundPublicMessages, + presentationDataPromise.get(), + searchStatePromise.get(), + selectionPromise.get(), + resolvedMessage, + fixedRecentlySearchedPeers, + foundThreads + ) + |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in + let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 || foundPublicMessages.1 var entries: [ChatListSearchEntry] = [] var index = 0 @@ -2629,6 +2757,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var firstHeaderId: Int64? if !foundRemotePeers.2 { index = 0 + var existingPostIds = Set() + for foundPublicMessageSet in foundPublicMessages.0 { + for message in foundPublicMessageSet.messages { + if existingPostIds.contains(message.id) { + continue + } + existingPostIds.insert(message.id) + + let headerId = listMessageDateHeaderId(timestamp: message.timestamp) + if firstHeaderId == nil { + firstHeaderId = headerId + } + let peer = EngineRenderedPeer(message: message) + entries.append(.message(message, peer, foundPublicMessageSet.readCounters[message.id.peerId], foundPublicMessageSet.threadsData[message.id]?.info, presentationData, foundPublicMessageSet.totalCount, nil, headerId == firstHeaderId, .index(message.index), nil, .publicPosts, false, nil, false)) + index += 1 + } + } + var existingMessageIds = Set() for foundRemoteMessageSet in foundRemoteMessages.0 { for message in foundRemoteMessageSet.messages { @@ -3131,6 +3277,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }) }, openStories: { peerId, avatarNode in strongSelf.interaction.openStories?(peerId, avatarNode) + }, openPublicPosts: { + strongSelf.interaction.switchToFilter(.publicPosts) }) strongSelf.currentEntries = newEntries if strongSelf.key == .downloads { @@ -3592,6 +3740,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { false )) } + + result.append(.footer(presentationData.theme, presentationData.strings.ChatList_Search_TopAppsInfo)) } var isEmpty = false @@ -3685,7 +3835,23 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } } else { - interaction.openPeer(peer, nil, threadId, true) + if isRecommended, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { + 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 { + interaction.openPeer(peer, nil, threadId, true) + } if threadId == nil { switch location { case .chatList, .forum: @@ -3710,6 +3876,49 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let _ = context.engine.peers.removeRecentlySearchedPeer(peerId: peerId).startStandalone() }, animationCache: strongSelf.animationCache, animationRenderer: strongSelf.animationRenderer, openStories: { peerId, avatarNode in interaction.openStories?(peerId, avatarNode) + }, openTopAppsInfo: { + var dismissImpl: (() -> Void)? + let alertController = textAlertController( + context: context, + title: presentationData.strings.TopApps_Info_Title, + text: presentationData.strings.TopApps_Info_Text, + actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.TopApps_Info_Done, action: {})], + parseMarkdown: true, + linkAction: { attributes, _ in + guard let self, let navigationController = self.navigationController else { + return + } + dismissImpl?() + if let value = attributes[NSAttributedString.Key(rawValue: "URL")] as? String { + if !value.isEmpty { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: value, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: navigationController, dismissInput: {}) + } else { + let _ = (context.engine.peers.resolvePeerByName(name: "botfather") + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let navigationController, let peer else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + context: context, + chatLocation: .peer(peer), + keepStack: .always + )) + }) + } + } + } + ) + interaction.present(alertController, nil) + dismissImpl = { [weak alertController] in + alertController?.dismissAnimated() + } }, isChannelsTabExpanded: recentItems.isChannelsTabExpanded, toggleChannelsTabExpanded: { @@ -4663,7 +4872,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let items = (0 ..< 2).compactMap { _ -> ListViewItem? in switch key { - case .chats, .topics, .channels, .apps, .downloads: + case .chats, .topics, .channels, .apps, .downloads, .publicPosts: let message = EngineMessage( stableId: 0, stableVersion: 0, diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 28bc9b2f8b..37f101cf1a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -12,6 +12,7 @@ import MultiAnimationRenderer protocol ChatListSearchPaneNode: ASDisplayNode { var isReady: Signal { get } + var isCurrent: Bool { get set } func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) func scrollToTop() -> Bool @@ -50,6 +51,7 @@ final class ChatListSearchPaneWrapper { public enum ChatListSearchPaneKey { case chats case topics + case publicPosts case channels case apps case media @@ -67,6 +69,8 @@ extension ChatListSearchPaneKey { return .chats case .topics: return .topics + case .publicPosts: + return .publicPosts case .channels: return .channels case .apps: @@ -87,13 +91,16 @@ extension ChatListSearchPaneKey { } } -func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool) -> [ChatListSearchPaneKey] { +func defaultAvailableSearchPanes(isForum: Bool, hasDownloads: Bool, hasPublicPosts: Bool) -> [ChatListSearchPaneKey] { var result: [ChatListSearchPaneKey] = [] if isForum { result.append(.topics) } else { result.append(.chats) } + if hasPublicPosts { + result.append(.publicPosts) + } result.append(.channels) result.append(.apps) result.append(contentsOf: [.media, .downloads, .links, .files, .music, .voice]) @@ -562,6 +569,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD }) } pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + pane.node.isCurrent = key == self.currentPaneKey if paneWasAdded && key == self.currentPaneKey { pane.node.didBecomeFocused() } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 852175365b..07ff564746 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2495,7 +2495,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } let firstRangeOrigin = chatListSearchResult.text.distance(from: chatListSearchResult.text.startIndex, to: firstRange.lowerBound) - if firstRangeOrigin > 24 { + if firstRangeOrigin > 24 && !chatListSearchResult.searchQuery.hasPrefix("#") { var leftOrigin: Int = 0 (composedString.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in let distanceFromEnd = firstRangeOrigin - range1.location diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 390608d0be..6536a80b34 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -509,6 +509,7 @@ public final class ChatPresentationInterfaceState: Equatable { public let showWebView: Bool public let currentSendAsPeerId: PeerId? public let copyProtectionEnabled: Bool + public let hasAtLeast3Messages: Bool public let hasPlentyOfMessages: Bool public let isPremium: Bool public let premiumGiftOptions: [CachedPremiumGiftOption] @@ -530,6 +531,7 @@ public final class ChatPresentationInterfaceState: Equatable { public let boostsToUnrestrict: Int32? public let businessIntro: TelegramBusinessIntro? public let hasBirthdayToday: Bool + public let adMessage: Message? public init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, threadData: ThreadData?, isGeneralThreadClosed: Bool?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, businessIntro: TelegramBusinessIntro?) { self.interfaceState = ChatInterfaceState() @@ -592,6 +594,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.showWebView = false self.currentSendAsPeerId = nil self.copyProtectionEnabled = false + self.hasAtLeast3Messages = false self.hasPlentyOfMessages = false self.isPremium = false self.premiumGiftOptions = [] @@ -613,9 +616,10 @@ public final class ChatPresentationInterfaceState: Equatable { self.boostsToUnrestrict = nil self.businessIntro = businessIntro self.hasBirthdayToday = false + self.adMessage = nil } - public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: UrlPreview?, editingUrlPreview: UrlPreview?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, historyFilter: HistoryFilter?, displayHistoryFilterAsList: Bool, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: (String, Data, String?)?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, forumTopicData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, savedMessagesTopicPeer: EnginePeer?, hasSearchTags: Bool, isPremiumRequiredForMessaging: Bool, hasSavedChats: Bool, appliedBoosts: Int32?, boostsToUnrestrict: Int32?, businessIntro: TelegramBusinessIntro?, hasBirthdayToday: Bool) { + public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: UrlPreview?, editingUrlPreview: UrlPreview?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, historyFilter: HistoryFilter?, displayHistoryFilterAsList: Bool, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: (String, Data, String?)?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasAtLeast3Messages: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, forumTopicData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, savedMessagesTopicPeer: EnginePeer?, hasSearchTags: Bool, isPremiumRequiredForMessaging: Bool, hasSavedChats: Bool, appliedBoosts: Int32?, boostsToUnrestrict: Int32?, businessIntro: TelegramBusinessIntro?, hasBirthdayToday: Bool, adMessage: Message?) { self.interfaceState = interfaceState self.chatLocation = chatLocation self.renderedPeer = renderedPeer @@ -676,6 +680,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.showWebView = showWebView self.currentSendAsPeerId = currentSendAsPeerId self.copyProtectionEnabled = copyProtectionEnabled + self.hasAtLeast3Messages = hasAtLeast3Messages self.hasPlentyOfMessages = hasPlentyOfMessages self.isPremium = isPremium self.premiumGiftOptions = premiumGiftOptions @@ -697,6 +702,7 @@ public final class ChatPresentationInterfaceState: Equatable { self.boostsToUnrestrict = boostsToUnrestrict self.businessIntro = businessIntro self.hasBirthdayToday = hasBirthdayToday + self.adMessage = adMessage } public static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -886,6 +892,9 @@ public final class ChatPresentationInterfaceState: Equatable { if lhs.copyProtectionEnabled != rhs.copyProtectionEnabled { return false } + if lhs.hasAtLeast3Messages != rhs.hasAtLeast3Messages { + return false + } if lhs.hasPlentyOfMessages != rhs.hasPlentyOfMessages { return false } @@ -949,35 +958,38 @@ public final class ChatPresentationInterfaceState: Equatable { if lhs.hasBirthdayToday != rhs.hasBirthdayToday { return false } + if lhs.adMessage?.id != rhs.adMessage?.id { + return false + } return true } public func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeer(_ f: (RenderedPeer?) -> RenderedPeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsNotAccessible(_ isNotAccessible: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedExplicitelyCanPinMessages(_ explicitelyCanPinMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedContactStatus(_ contactStatus: ChatContactStatus?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasBots(_ hasBots: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsArchived(_ isArchived: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -989,271 +1001,279 @@ public final class ChatPresentationInterfaceState: Equatable { inputQueryResults.removeValue(forKey: queryKind) } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedEditMessageState(_ editMessageState: ChatEditInterfaceMessageState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPinnedMessage(_ pinnedMessage: ChatPinnedMessage?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeerDiscussionId(_ peerDiscussionId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCallsAvailable(_ callsAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCallsPrivate(_ callsPrivate: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSlowmodeState(_ slowmodeState: ChatSlowmodeState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedUrlPreview(_ urlPreview: UrlPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedEditingUrlPreview(_ editingUrlPreview: UrlPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSearchQuerySuggestionResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHistoryFilter(_ historyFilter: HistoryFilter?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedDisplayHistoryFilterAsList(_ displayHistoryFilterAsList: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedMode(_ mode: ChatControllerPresentationMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPresentationReady(_ presentationReady: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedTheme(_ theme: PresentationTheme) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedStrings(_ strings: PresentationStrings) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedDateTimeFormat(_ dateTimeFormat: PresentationDateTimeFormat) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedChatWallpaper(_ chatWallpaper: TelegramWallpaper) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBubbleCorners(_ bubbleCorners: PresentationChatBubbleCorners) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSubject(_ subject: ChatControllerSubject?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedAutoremoveTimeout(_ autoremoveTimeout: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPendingUnpinnedAllMessages(_ pendingUnpinnedAllMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedActiveGroupCallInfo(_ activeGroupCallInfo: ChatActiveGroupCallInfo?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasActiveGroupCall(_ hasActiveGroupCall: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedImportState(_ importState: ChatPresentationImportState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedReportReason(_ reportReason: (String, Data, String?)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedShowCommands(_ showCommands: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasBotCommands(_ hasBotCommands: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedShowSendAsPeers(_ showSendAsPeers: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSendAsPeers(_ sendAsPeers: [SendAsPeer]?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCurrentSendAsPeerId(_ currentSendAsPeerId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBotMenuButton(_ botMenuButton: BotMenuButton) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedShowWebView(_ showWebView: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCopyProtectionEnabled(_ copyProtectionEnabled: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) + } + + public func updatedHasAtLeast3Messages(_ hasAtLeast3Messages: Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasPlentyOfMessages(_ hasPlentyOfMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsPremium(_ isPremium: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSuggestPremiumGift(_ suggestPremiumGift: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedForceInputCommandsHidden(_ forceInputCommandsHidden: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedCustomEmojiAvailable(_ customEmojiAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedThreadData(_ threadData: ThreadData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedForumTopicData(_ forumTopicData: ThreadData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsGeneralThreadClosed(_ isGeneralThreadClosed: Bool?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedTranslationState(_ translationState: ChatPresentationTranslationState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedReplyMessage(_ replyMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedAccountPeerColor(_ accountPeerColor: AccountPeerColor?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedSavedMessagesTopicPeer(_ savedMessagesTopicPeer: EnginePeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasSearchTags(_ hasSearchTags: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedIsPremiumRequiredForMessaging(_ isPremiumRequiredForMessaging: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasSavedChats(_ hasSavedChats: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedAppliedBoosts(_ appliedBoosts: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBoostsToUnrestrict(_ boostsToUnrestrict: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedBusinessIntro(_ businessIntro: TelegramBusinessIntro?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: businessIntro, hasBirthdayToday: self.hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage) } public func updatedHasBirthdayToday(_ hasBirthdayToday: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: hasBirthdayToday) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: hasBirthdayToday, adMessage: self.adMessage) + } + + public func updatedAdMessage(_ adMessage: Message?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: adMessage) } } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index b42f21063e..53dcb81b79 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -748,11 +748,15 @@ public struct ComponentTransition { } public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animateScale(layer: view.layer, from: fromValue, to: toValue, delay: delay, additive: additive, completion: completion) + } + + public func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: completion?(true) case let .curve(duration, curve): - view.layer.animate( + layer.animate( from: fromValue as NSNumber, to: toValue as NSNumber, keyPath: "transform.scale", diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 1ea0212393..7a3caa1c4e 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -13,17 +13,19 @@ public final class RoundedRectangle: Component { public let gradientDirection: GradientDirection public let stroke: CGFloat? public let strokeColor: UIColor? + public let size: CGSize? - public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { - self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor) + public convenience init(color: UIColor, cornerRadius: CGFloat?, stroke: CGFloat? = nil, strokeColor: UIColor? = nil, size: CGSize? = nil) { + self.init(colors: [color], cornerRadius: cornerRadius, stroke: stroke, strokeColor: strokeColor, size: size) } - public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil) { + public init(colors: [UIColor], cornerRadius: CGFloat?, gradientDirection: GradientDirection = .horizontal, stroke: CGFloat? = nil, strokeColor: UIColor? = nil, size: CGSize? = nil) { self.colors = colors self.cornerRadius = cornerRadius self.gradientDirection = gradientDirection self.stroke = stroke self.strokeColor = strokeColor + self.size = size } public static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool { @@ -42,6 +44,9 @@ public final class RoundedRectangle: Component { if lhs.strokeColor != rhs.strokeColor { return false } + if lhs.size != rhs.size { + return false + } return true } @@ -49,8 +54,9 @@ public final class RoundedRectangle: Component { var component: RoundedRectangle? func update(component: RoundedRectangle, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + let size = component.size ?? availableSize if self.component != component { - let cornerRadius = component.cornerRadius ?? min(availableSize.width, availableSize.height) * 0.5 + let cornerRadius = component.cornerRadius ?? min(size.width, size.height) * 0.5 if component.colors.count == 1, let color = component.colors.first { let imageSize = CGSize(width: max(component.stroke ?? 0.0, cornerRadius) * 2.0, height: max(component.stroke ?? 0.0, cornerRadius) * 2.0) @@ -75,7 +81,7 @@ public final class RoundedRectangle: Component { self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(cornerRadius), topCapHeight: Int(cornerRadius)) UIGraphicsEndImageContext() } else if component.colors.count > 1 { - let imageSize = availableSize + let imageSize = size UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) if let context = UIGraphicsGetCurrentContext() { context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: cornerRadius).cgPath) @@ -106,7 +112,7 @@ public final class RoundedRectangle: Component { } } - return availableSize + return size } } @@ -120,13 +126,18 @@ public final class RoundedRectangle: Component { } public final class FilledRoundedRectangleComponent: Component { + public enum CornerRadius: Equatable { + case value(CGFloat) + case minEdge + } + public let color: UIColor - public let cornerRadius: CGFloat + public let cornerRadius: CornerRadius public let smoothCorners: Bool public init( color: UIColor, - cornerRadius: CGFloat, + cornerRadius: CornerRadius, smoothCorners: Bool ) { self.color = color @@ -210,9 +221,17 @@ public final class FilledRoundedRectangleComponent: Component { transition.setTintColor(view: self, color: component.color) - if self.currentCornerRadius != component.cornerRadius { + let cornerRadius: CGFloat + switch component.cornerRadius { + case let .value(value): + cornerRadius = value + case .minEdge: + cornerRadius = min(availableSize.width, availableSize.height) * 0.5 + } + + if self.currentCornerRadius != cornerRadius { let previousCornerRadius = self.currentCornerRadius - self.currentCornerRadius = component.cornerRadius + self.currentCornerRadius = cornerRadius if transition.animation.isImmediate { self.applyStaticCornerRadius() } else { @@ -230,7 +249,7 @@ public final class FilledRoundedRectangleComponent: Component { } } - transition.setCornerRadius(layer: self.layer, cornerRadius: component.cornerRadius, completion: { [weak self] completed in + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in guard let self, completed else { return } diff --git a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift index 0d7dd99769..30453c2122 100644 --- a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift +++ b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift @@ -23,6 +23,7 @@ public final class BalancedTextComponent: Component { public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let highlightInset: UIEdgeInsets public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -41,6 +42,7 @@ public final class BalancedTextComponent: Component { textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + highlightInset: UIEdgeInsets = .zero, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -58,6 +60,7 @@ public final class BalancedTextComponent: Component { self.textShadowBlur = textShadowBlur self.textStroke = textStroke self.highlightColor = highlightColor + self.highlightInset = highlightInset self.highlightAction = highlightAction self.tapAction = tapAction self.longTapAction = longTapAction @@ -122,6 +125,10 @@ public final class BalancedTextComponent: Component { return false } + if lhs.highlightInset != rhs.highlightInset { + return false + } + return true } @@ -165,6 +172,7 @@ public final class BalancedTextComponent: Component { self.textView.textShadowBlur = component.textShadowBlur self.textView.textStroke = component.textStroke self.textView.linkHighlightColor = component.highlightColor + self.textView.linkHighlightInset = component.highlightInset self.textView.highlightAttributeAction = component.highlightAction self.textView.tapAttributeAction = component.tapAction self.textView.longTapAttributeAction = component.longTapAction diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index 4a1f588656..cfcbaf9b85 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -22,6 +22,7 @@ public final class MultilineTextComponent: Component { public let textShadowBlur: CGFloat? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let highlightInset: UIEdgeInsets public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? @@ -39,6 +40,7 @@ public final class MultilineTextComponent: Component { textShadowBlur: CGFloat? = nil, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + highlightInset: UIEdgeInsets = .zero, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil @@ -55,6 +57,7 @@ public final class MultilineTextComponent: Component { self.textShadowBlur = textShadowBlur self.textStroke = textStroke self.highlightColor = highlightColor + self.highlightInset = highlightInset self.highlightAction = highlightAction self.tapAction = tapAction self.longTapAction = longTapAction @@ -116,6 +119,10 @@ public final class MultilineTextComponent: Component { return false } + if lhs.highlightInset != rhs.highlightInset { + return false + } + return true } @@ -143,6 +150,7 @@ public final class MultilineTextComponent: Component { self.textShadowBlur = component.textShadowBlur self.textStroke = component.textStroke self.linkHighlightColor = component.highlightColor + self.linkHighlightInset = component.highlightInset self.highlightAttributeAction = component.highlightAction self.tapAttributeAction = component.tapAction self.longTapAttributeAction = component.longTapAction diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 7ad116ee03..9b450ce4ac 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -367,20 +367,24 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { struct Counter: Equatable { var components: [CounterLayout.Component] + var extractedComponents: [CounterLayout.Component] } struct Layout: Equatable { var colors: Colors var size: CGSize + var extractedSize: CGSize var counter: Counter? var isTag: Bool } private struct AnimationState { var fromCounter: Counter? + var fromExtracted: Bool var fromColors: Colors var startTime: Double var duration: Double + var curve: ComponentTransition.Animation.Curve } private var isExtracted: Bool = false @@ -391,6 +395,9 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { override init(frame: CGRect) { super.init(frame: CGRect()) + + self.layer.contentsScale = UIScreenScale + self.layer.contentsGravity = .topLeft } required init?(coder: NSCoder) { @@ -405,13 +412,12 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { func update(layout: Layout) { if self.currentLayout != layout { if let currentLayout = self.currentLayout, (currentLayout.counter != layout.counter || currentLayout.colors.isSelected != layout.colors.isSelected) { - self.animationState = AnimationState(fromCounter: currentLayout.counter, fromColors: currentLayout.colors, startTime: CACurrentMediaTime(), duration: 0.15 * UIView.animationDurationFactor()) + self.animationState = AnimationState(fromCounter: currentLayout.counter, fromExtracted: self.isExtracted, fromColors: currentLayout.colors, startTime: CACurrentMediaTime(), duration: 0.15 * UIView.animationDurationFactor(), curve: .linear) } self.currentLayout = layout self.updateBackgroundImage(animated: false) - self.updateAnimation() } } @@ -447,7 +453,21 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { func updateIsExtracted(isExtracted: Bool, animated: Bool) { if self.isExtracted != isExtracted { self.isExtracted = isExtracted - self.updateBackgroundImage(animated: animated) + + if let currentLayout = self.currentLayout { + self.animationState = AnimationState( + fromCounter: currentLayout.counter, + fromExtracted: !isExtracted, + fromColors: currentLayout.colors, + startTime: CACurrentMediaTime(), + duration: 0.5 * UIView.animationDurationFactor(), + curve: .spring + ) + self.updateBackgroundImage(animated: false) + updateAnimation() + } else { + self.updateBackgroundImage(animated: true) + } } } @@ -456,9 +476,18 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { return } - var totalComponentWidth: CGFloat = 0.0 + var counterComponents: [CounterLayout.Component]? if let counter = layout.counter { - for component in counter.components { + if self.isExtracted { + counterComponents = counter.extractedComponents + } else { + counterComponents = counter.components + } + } + + var totalComponentWidth: CGFloat = 0.0 + if let counterComponents { + for component in counterComponents { totalComponentWidth += component.bounds.width } } @@ -466,8 +495,30 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let isExtracted = self.isExtracted let animationState = self.animationState + var animationFraction: CGFloat + var fixedTransitionDirection: Bool? + if let animationState, animationState.fromCounter != nil { + animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration)) + animationFraction = animationState.curve.solve(at: animationFraction) + if animationState.fromExtracted != isExtracted { + fixedTransitionDirection = isExtracted ? true : false + } + } else { + animationFraction = 1.0 + } + + let targetImageSize = isExtracted ? layout.extractedSize : layout.size + var imageSize = targetImageSize + if let animationState { + let sourceImageSize = animationState.fromExtracted ? layout.extractedSize : layout.size + imageSize = CGSize( + width: floor(sourceImageSize.width * (1.0 - animationFraction) + targetImageSize.width * animationFraction), + height: floor(sourceImageSize.height * (1.0 - animationFraction) + targetImageSize.height * animationFraction) + ) + } + DispatchQueue.global().async { [weak self] in - let image = generateImage(layout.size, rotatedContext: { size, context in + let image = generateImage(imageSize, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) @@ -511,7 +562,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) } - if let counter = layout.counter { + if let counterComponents { let isForegroundTransparent = foregroundColor.alpha < 1.0 context.setBlendMode(isForegroundTransparent ? .copy : .normal) @@ -522,39 +573,53 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { textOrigin = 36.0 } - var rightTextOrigin = textOrigin + totalComponentWidth + var leftTextOrigin = textOrigin - let animationFraction: CGFloat - if let animationState = animationState, animationState.fromCounter != nil { - animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration)) - } else { - animationFraction = 1.0 - } - - for i in (0 ..< counter.components.count).reversed() { - let component = counter.components[i] + for i in 0 ..< counterComponents.count { + let component = counterComponents[i] var componentAlpha: CGFloat = 1.0 var componentVerticalOffset: CGFloat = 0.0 + var componentAnimationFraction = animationFraction + if let animationState = animationState, let fromCounter = animationState.fromCounter { - let reverseIndex = counter.components.count - 1 - i - if reverseIndex < fromCounter.components.count { - let previousComponent = fromCounter.components[fromCounter.components.count - 1 - reverseIndex] + let fromCounterComponents = animationState.fromExtracted ? fromCounter.extractedComponents : fromCounter.components + + let countNorm = max(counterComponents.count, fromCounterComponents.count) + let countFraction = CGFloat(i + 1) / CGFloat(countNorm) + + let minDurationCompression = 0.25 + let maxDurationCompression = 1.0 + + let durationCompression = minDurationCompression * (1.0 - countFraction) + maxDurationCompression * countFraction + + let adjustedDuration = animationState.duration * durationCompression + + componentAnimationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / adjustedDuration)) + componentAnimationFraction = animationState.curve.solve(at: componentAnimationFraction) + + if i < fromCounterComponents.count { + let previousComponent = fromCounterComponents[i] if previousComponent != component { - componentAlpha = animationFraction - componentVerticalOffset = -(1.0 - animationFraction) * 8.0 - if previousComponent.string < component.string { - componentVerticalOffset = -componentVerticalOffset - } - + componentAlpha = componentAnimationFraction + componentVerticalOffset = -(1.0 - componentAnimationFraction) * 12.0 let previousComponentAlpha = 1.0 - componentAlpha - var previousComponentVerticalOffset = animationFraction * 8.0 - if previousComponent.string < component.string { - previousComponentVerticalOffset = -previousComponentVerticalOffset + var previousComponentVerticalOffset = componentAnimationFraction * 12.0 + + if let fixedTransitionDirection { + if !fixedTransitionDirection { + componentVerticalOffset = -componentVerticalOffset + previousComponentVerticalOffset = -previousComponentVerticalOffset + } + } else { + if previousComponent.string < component.string { + componentVerticalOffset = -componentVerticalOffset + previousComponentVerticalOffset = -previousComponentVerticalOffset + } } - var componentOrigin = rightTextOrigin - previousComponent.bounds.width + var componentOrigin = leftTextOrigin componentOrigin = max(componentOrigin, layout.size.height / 2.0 + UIScreenPixel) let previousColor: UIColor if isForegroundTransparent { @@ -565,10 +630,18 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let string = NSAttributedString(string: previousComponent.string, font: Font.medium(11.0), textColor: previousColor) string.draw(at: previousComponent.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - previousComponent.bounds.height) / 2.0 + previousComponentVerticalOffset)) } + } else { + componentAlpha = componentAnimationFraction + componentVerticalOffset = -(1.0 - componentAnimationFraction) * 12.0 + if let fixedTransitionDirection { + if !fixedTransitionDirection { + componentVerticalOffset = -componentVerticalOffset + } + } } } - let componentOrigin = rightTextOrigin - component.bounds.width + let componentOrigin = leftTextOrigin let currentColor: UIColor if isForegroundTransparent { currentColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - componentAlpha) @@ -578,7 +651,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: currentColor) string.draw(at: component.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0 + componentVerticalOffset)) - rightTextOrigin -= component.bounds.width + leftTextOrigin += component.bounds.width } } } @@ -591,6 +664,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { if !layout.colors.isSelected { animationFraction = 1.0 - animationFraction } + animationFraction = animationState.curve.solve(at: animationFraction) let center = CGPoint(x: 21.0, y: size.height / 2.0) let diameter = 0.0 * (1.0 - animationFraction) + (size.width - center.x) * 2.0 * animationFraction @@ -619,7 +693,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { if let strongSelf = self, let image = image { let previousContents = strongSelf.layer.contents - ASDisplayNodeSetResizableContents(strongSelf.layer, image) + //ASDisplayNodeSetResizableContents(strongSelf.layer, image) + strongSelf.layer.contents = image.cgImage if animated, let previousContents = previousContents { strongSelf.layer.animate(from: previousContents as! CGImage, to: image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) @@ -714,10 +789,12 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let imageSize: CGSize let counterLayout: CounterLayout? + let extractedCounterLayout: CounterLayout? let backgroundLayout: ContainerButtonNode.Layout let size: CGSize + let extractedSize: CGSize init( spec: Spec, @@ -726,8 +803,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { imageFrame: CGRect, imageSize: CGSize, counterLayout: CounterLayout?, + extractedCounterLayout: CounterLayout?, backgroundLayout: ContainerButtonNode.Layout, - size: CGSize + size: CGSize, + extractedSize: CGSize ) { self.spec = spec self.backgroundColor = backgroundColor @@ -735,8 +814,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { self.imageFrame = imageFrame self.imageSize = imageSize self.counterLayout = counterLayout + self.extractedCounterLayout = extractedCounterLayout self.backgroundLayout = backgroundLayout self.size = size + self.extractedSize = extractedSize } static func calculate(spec: Spec, currentLayout: Layout?) -> Layout { @@ -748,14 +829,25 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { let imageSize: CGSize = boundingImageSize var counterComponents: [String] = [] + var extractedCounterComponents: [String] = [] var hasTitle = false if let title = spec.component.reaction.title, !title.isEmpty { hasTitle = true counterComponents.append(title) + extractedCounterComponents.append(title) } else { - for character in countString(Int64(spec.component.count)) { + #if DEBUG && false + let compactString = "4K" + #else + let compactString = countString(Int64(spec.component.count)) + #endif + + for character in compactString { counterComponents.append(String(character)) } + for character in "\(spec.component.count)" { + extractedCounterComponents.append(String(character)) + } } let backgroundColor = spec.component.chosenOrder != nil ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground @@ -768,8 +860,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } var counterLayout: CounterLayout? + var extractedCounterLayout: CounterLayout? var size = CGSize(width: boundingImageSize.width + sideInsets * 2.0, height: height) + var extractedSize = size if !spec.component.avatarPeers.isEmpty { size.width += 4.0 + 24.0 if spec.component.avatarPeers.count > 1 { @@ -777,25 +871,48 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } else { size.width -= 2.0 } + extractedSize = size } else if spec.component.isTag && !hasTitle { size.width += 1.0 + extractedSize = size } else { - let counterSpec = CounterLayout.Spec( - stringComponents: counterComponents - ) - let counterValue: CounterLayout - if let currentCounter = currentLayout?.counterLayout, currentCounter.spec == counterSpec { - counterValue = currentCounter - } else { - counterValue = CounterLayout.calculate( - spec: counterSpec, - previousLayout: currentLayout?.counterLayout + do { + let counterSpec = CounterLayout.Spec( + stringComponents: counterComponents ) + let counterValue: CounterLayout + if let currentCounter = currentLayout?.counterLayout, currentCounter.spec == counterSpec { + counterValue = currentCounter + } else { + counterValue = CounterLayout.calculate( + spec: counterSpec, + previousLayout: currentLayout?.counterLayout + ) + } + counterLayout = counterValue + size.width += spacing + counterValue.size.width + if spec.component.isTag { + size.width += 5.0 + } } - counterLayout = counterValue - size.width += spacing + counterValue.size.width - if spec.component.isTag { - size.width += 5.0 + do { + let extractedCounterSpec = CounterLayout.Spec( + stringComponents: extractedCounterComponents + ) + let extractedCounterValue: CounterLayout + if let currentExtractedCounter = currentLayout?.extractedCounterLayout, currentExtractedCounter.spec == extractedCounterSpec { + extractedCounterValue = currentExtractedCounter + } else { + extractedCounterValue = CounterLayout.calculate( + spec: extractedCounterSpec, + previousLayout: currentLayout?.extractedCounterLayout + ) + } + extractedCounterLayout = extractedCounterValue + extractedSize.width += spacing + extractedCounterValue.size.width + if spec.component.isTag { + extractedSize.width += 5.0 + } } } @@ -821,14 +938,16 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { ) } var backgroundCounter: ReactionButtonAsyncNode.ContainerButtonNode.Counter? - if let counterLayout = counterLayout { + if let counterLayout, let extractedCounterLayout { backgroundCounter = ReactionButtonAsyncNode.ContainerButtonNode.Counter( - components: counterLayout.components + components: counterLayout.components, + extractedComponents: extractedCounterLayout.components ) } let backgroundLayout = ContainerButtonNode.Layout( colors: backgroundColors, size: size, + extractedSize: extractedSize, counter: backgroundCounter, isTag: spec.component.isTag ) @@ -840,8 +959,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { imageFrame: imageFrame, imageSize: boundingImageSize, counterLayout: counterLayout, + extractedCounterLayout: extractedCounterLayout, backgroundLayout: backgroundLayout, - size: size + size: size, + extractedSize: extractedSize ) } } @@ -1056,9 +1177,10 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView { } fileprivate func apply(layout: Layout, animation: ListViewItemUpdateAnimation, arguments: ReactionButtonsAsyncLayoutContainer.Arguments) { - self.containerView.frame = CGRect(origin: CGPoint(), size: layout.size) - self.containerView.contentView.frame = CGRect(origin: CGPoint(), size: layout.size) - self.containerView.contentRect = CGRect(origin: CGPoint(), size: layout.size) + self.containerView.frame = CGRect(origin: CGPoint(), size: layout.extractedSize) + self.containerView.contentView.frame = CGRect(origin: CGPoint(), size: layout.extractedSize) + self.containerView.contentRect = CGRect(origin: CGPoint(), size: layout.extractedSize) + let buttonFrame = CGRect(origin: CGPoint(), size: layout.size) animation.animator.updatePosition(layer: self.buttonNode.layer, position: buttonFrame.center, completion: nil) animation.animator.updateBounds(layer: self.buttonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil) diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 12346ecdb7..d15bf6e006 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -103,6 +103,16 @@ public struct ContactsPeerItemAction { } } +public struct ContactsPeerItemButtonAction { + public let title: String + public let action: ((ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void)? + + public init(title: String, action: @escaping (ContactsPeerItemPeer, ASDisplayNode, ContextGesture?) -> Void) { + self.title = title + self.action = action + } +} + public enum ContactsPeerItemPeer: Equatable { case thread(peer: EnginePeer, title: String, icon: Int64?, color: Int32) case peer(peer: EnginePeer?, chatPeer: EnginePeer?) @@ -175,7 +185,9 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let options: [ItemListPeerItemRevealOption] let additionalActions: [ContactsPeerItemAction] let actionIcon: ContactsPeerItemActionIcon + let buttonAction: ContactsPeerItemButtonAction? let searchQuery: String? + let alwaysShowLastSeparator: Bool let action: ((ContactsPeerItemPeer) -> Void)? let disabledAction: ((ContactsPeerItemPeer) -> Void)? let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? @@ -214,9 +226,11 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], actionIcon: ContactsPeerItemActionIcon = .none, + buttonAction: ContactsPeerItemButtonAction? = nil, index: SortIndex?, header: ListViewItemHeader?, searchQuery: String? = nil, + alwaysShowLastSeparator: Bool = false, action: ((ContactsPeerItemPeer) -> Void)?, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil, @@ -247,7 +261,9 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.options = options self.additionalActions = additionalActions self.actionIcon = actionIcon + self.buttonAction = buttonAction self.searchQuery = searchQuery + self.alwaysShowLastSeparator = alwaysShowLastSeparator self.action = action self.disabledAction = disabledAction self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -430,6 +446,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var arrowButtonNode: HighlightableButtonNode? private var rightLabelTextNode: TextNode? + private var actionButtonNode: HighlightTrackingButtonNode? + private var actionButtonTitleNode: TextNode? + private var actionButtonBackgroundNode: ASImageNode? + private var avatarTapRecognizer: UITapGestureRecognizer? private var isHighlighted: Bool = false @@ -693,6 +713,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let makeBadgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let makeRightLabelTextLayout = TextNode.asyncLayout(self.rightLabelTextNode) + let makeActionButtonTitleLayuout = TextNode.asyncLayout(self.actionButtonTitleNode) + let currentItem = self.layoutParams?.0 return { [weak self] item, params, first, last, firstWithHeader, neighbors in @@ -1027,6 +1049,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusHeightComponent = -1.0 + statusLayout.size.height } + var actionButtonTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)? + if let buttonAction = item.buttonAction { + actionButtonTitleLayoutAndApply = makeActionButtonTitleLayuout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonAction.title, font: Font.semibold(15.0), textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + } + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + statusHeightComponent), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) let titleFrame: CGRect @@ -1411,6 +1438,73 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { verifiedIconView.removeFromSuperview() } + var additionalRightInset: CGFloat = 0.0 + if let (titleLayout, titleApply) = actionButtonTitleLayoutAndApply { + let actionButtonTitleNode = titleApply() + let actionButtonBackgroundNode: ASImageNode + let actionButtonNode: HighlightTrackingButtonNode + if let currentBackgroundNode = strongSelf.actionButtonBackgroundNode, let currentButtonNode = strongSelf.actionButtonNode { + actionButtonBackgroundNode = currentBackgroundNode + actionButtonNode = currentButtonNode + } else { + actionButtonBackgroundNode = ASImageNode() + actionButtonBackgroundNode.displaysAsynchronously = false + strongSelf.offsetContainerNode.addSubnode(actionButtonBackgroundNode) + strongSelf.actionButtonBackgroundNode = actionButtonBackgroundNode + + actionButtonNode = HighlightTrackingButtonNode() + actionButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.actionButtonTitleNode?.layer.removeAnimation(forKey: "opacity") + strongSelf.actionButtonTitleNode?.alpha = 0.4 + strongSelf.actionButtonBackgroundNode?.layer.removeAnimation(forKey: "opacity") + strongSelf.actionButtonBackgroundNode?.alpha = 0.4 + } else { + strongSelf.actionButtonTitleNode?.alpha = 1.0 + strongSelf.actionButtonTitleNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.actionButtonBackgroundNode?.alpha = 1.0 + strongSelf.actionButtonBackgroundNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed(_:)), forControlEvents: .touchUpInside) + + strongSelf.offsetContainerNode.addSubnode(actionButtonNode) + strongSelf.actionButtonNode = actionButtonNode + } + if strongSelf.actionButtonTitleNode == nil { + strongSelf.actionButtonTitleNode = actionButtonTitleNode + strongSelf.offsetContainerNode.insertSubnode(actionButtonTitleNode, aboveSubnode: actionButtonBackgroundNode) + } + if updatedTheme != nil || actionButtonBackgroundNode.image == nil { + actionButtonBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.list.itemAccentColor) + } + + let actionButtonSize = CGSize(width: titleLayout.size.width + 13.0 * 2.0, height: 28.0) + let actionButtonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 12.0 - actionButtonSize.width, y: floorToScreenPixels((nodeLayout.contentSize.height - actionButtonSize.height) / 2.0)), size: actionButtonSize) + actionButtonBackgroundNode.frame = actionButtonFrame + actionButtonNode.frame = actionButtonFrame + + let actionTitleFrame = CGRect(origin: CGPoint(x: actionButtonFrame.minX + 13.0, y: actionButtonFrame.minY + floorToScreenPixels((actionButtonFrame.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size) + actionButtonTitleNode.frame = actionTitleFrame + + additionalRightInset += actionButtonSize.width + 16.0 + } else { + if let actionButtonTitleNode = strongSelf.actionButtonTitleNode { + strongSelf.actionButtonTitleNode = nil + actionButtonTitleNode.removeFromSupernode() + } + if let actionButtonBackgroundNode = strongSelf.actionButtonBackgroundNode { + strongSelf.actionButtonBackgroundNode = nil + actionButtonBackgroundNode.removeFromSupernode() + } + if let actionButtonNode = strongSelf.actionButtonNode { + strongSelf.actionButtonNode = nil + actionButtonNode.removeFromSupernode() + } + } + if let actionButtons, actionButtons.count == 1, let actionButton = actionButtons.first, case .more = actionButton.type { let moreButtonNode: MoreButtonNode if let current = strongSelf.moreButtonNode { @@ -1426,7 +1520,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { actionButton.action?(item.peer, sourceNode, gesture) } let moreButtonSize = moreButtonNode.measure(CGSize(width: 100.0, height: nodeLayout.contentSize.height)) - moreButtonNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 21.0 - moreButtonSize.width, y:floor((nodeLayout.contentSize.height - moreButtonSize.height) / 2.0)), size: moreButtonSize) + moreButtonNode.frame = CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 21.0 - moreButtonSize.width, y: floor((nodeLayout.contentSize.height - moreButtonSize.height) / 2.0)), size: moreButtonSize) } else if let actionButtons = actionButtons { if strongSelf.actionButtonNodes == nil { var actionButtonNodes: [HighlightableButtonNode] = [] @@ -1497,7 +1591,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { badgeBackgroundNode.image = currentBadgeBackgroundImage badgeBackgroundWidth = max(badgeTextLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width) - var badgeBackgroundFrame = CGRect(x: revealOffset + params.width - params.rightInset - badgeBackgroundWidth - 6.0, y: floor((nodeLayout.contentSize.height - currentBadgeBackgroundImage.size.height) / 2.0), width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) + var badgeBackgroundFrame = CGRect(x: revealOffset + params.width - params.rightInset - badgeBackgroundWidth - additionalRightInset - 6.0, y: floor((nodeLayout.contentSize.height - currentBadgeBackgroundImage.size.height) / 2.0), width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) if let arrowButtonImage = arrowButtonImage { badgeBackgroundFrame.origin.x -= arrowButtonImage.size.width + 6.0 @@ -1587,7 +1681,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(nodeLayout.insets.top, separatorHeight)), size: CGSize(width: nodeLayout.contentSize.width, height: separatorHeight)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - leftInset), height: separatorHeight)) - strongSelf.separatorNode.isHidden = last + if !item.alwaysShowLastSeparator { + strongSelf.separatorNode.isHidden = last + } if let userPresence = userPresence { strongSelf.peerPresenceManager?.reset(presence: userPresence) @@ -1615,7 +1711,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } @objc private func actionButtonPressed(_ sender: HighlightableButtonNode) { - guard let actionButtonNodes = self.actionButtonNodes, let index = actionButtonNodes.firstIndex(of: sender), let item = self.item, index < item.additionalActions.count else { + guard let item = self.item else { + return + } + if let action = item.buttonAction { + action.action?(item.peer, sender, nil) + return + } + guard let actionButtonNodes = self.actionButtonNodes, let index = actionButtonNodes.firstIndex(of: sender), index < item.additionalActions.count else { return } item.additionalActions[index].action?(item.peer, sender, nil) diff --git a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift index ee65de32c0..909a81bfcd 100644 --- a/submodules/ContextUI/Sources/ContextActionsContainerNode.swift +++ b/submodules/ContextUI/Sources/ContextActionsContainerNode.swift @@ -104,8 +104,9 @@ private final class InnerActionsContainerNode: ASDisplayNode { } } case let .custom(item, _): - itemNodes.append(.custom(item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected))) - if i != items.count - 1 { + let itemNode = item.node(presentationData: presentationData, getController: getController, actionSelected: actionSelected) + itemNodes.append(.custom(itemNode)) + if i != items.count - 1 && itemNode.needsSeparator { switch items[i + 1] { case .action, .custom: let separatorNode = ASDisplayNode() @@ -442,6 +443,12 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { self.targetSelectionIndex = nil icon = nil isUserInteractionEnabled = action != nil + case .videoProcessing: + self.action = nil + self.text = "The video will be published once converted and optimized." + self.targetSelectionIndex = nil + icon = nil + isUserInteractionEnabled = action != nil } self.iconNode = ASImageNode() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 05c6be0606..0a39323617 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -226,6 +226,14 @@ public protocol ContextMenuCustomNode: ASDisplayNode { func canBeHighlighted() -> Bool func updateIsHighlighted(isHighlighted: Bool) func performAction() + + var needsSeparator: Bool { get } +} + +public extension ContextMenuCustomNode { + var needsSeparator: Bool { + return true + } } public protocol ContextMenuCustomItem { @@ -2350,6 +2358,7 @@ public final class ContextController: ViewController, StandalonePresentableContr case animatedEmoji(text: String?, arguments: TextNodeWithEntities.Arguments?, file: TelegramMediaFile?, action: (() -> Void)?) case notificationTopicExceptions(text: String, action: (() -> Void)?) case starsReactions(topCount: Int) + case videoProcessing public static func ==(lhs: Tip, rhs: Tip) -> Bool { switch lhs { @@ -2401,6 +2410,12 @@ public final class ContextController: ViewController, StandalonePresentableContr } else { return false } + case .videoProcessing: + if case .videoProcessing = rhs { + return true + } else { + return false + } } } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index c1feba5545..71efef132d 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -629,7 +629,7 @@ private final class ContextControllerActionsListCustomItemNode: ASDisplayNode, C private let requestDismiss: (ContextMenuActionResult) -> Void private var presentationData: PresentationData? - private var itemNode: ContextMenuCustomNode? + private(set) var itemNode: ContextMenuCustomNode? init( getController: @escaping () -> ContextControllerProtocol?, @@ -862,18 +862,28 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio if let separatorNode = item.separatorNode { itemTransition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.maxY), size: CGSize(width: itemFrame.width, height: UIScreenPixel)), beginWithCurrentState: true) + + var separatorHidden = false if i != self.itemNodes.count - 1 { switch self.items[i + 1] { case .separator: - separatorNode.isHidden = true + separatorHidden = true case .action: - separatorNode.isHidden = false + separatorHidden = false case .custom: - separatorNode.isHidden = false + separatorHidden = false } } else { - separatorNode.isHidden = true + separatorHidden = true } + + if let itemContainerNode = item.node as? ContextControllerActionsListCustomItemNode, let itemNode = itemContainerNode.itemNode { + if !itemNode.needsSeparator { + separatorHidden = true + } + } + + separatorNode.isHidden = separatorHidden } itemNodeLayout.apply(itemSize, itemTransition) diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index db37d2bed4..d6b9f206a1 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -285,7 +285,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { } else { UIPasteboard.general.setData(data, forPasteboardType: dataType) } - context.sharedContext.openResolvedUrl(.importStickers, context: context, urlContext: .generic, navigationController: arguments.getNavigationController(), forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { c, a in arguments.presentController(c, a as? ViewControllerPresentationArguments) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) + context.sharedContext.openResolvedUrl(.importStickers, context: context, urlContext: .generic, navigationController: arguments.getNavigationController(), forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { c, a in arguments.presentController(c, a as? ViewControllerPresentationArguments) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) } }) case .sendLogs: diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index cfa335f52d..1b8b3e4d11 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -100,7 +100,7 @@ public final class DeviceAccess { public static func isCameraAccessAuthorized() -> Bool { return AVCaptureDevice.authorizationStatus(for: .video) == .authorized } - + public static func authorizationStatus(applicationInForeground: Signal? = nil, siriAuthorization: (() -> AccessType)? = nil, subject: DeviceAccessSubject) -> Signal { switch subject { case .notifications: diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 4ec3efdd87..6b49b01437 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -273,6 +273,24 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAdditive(layer: CALayer, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if layer.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + layer.frame = frame + if let completion = completion { + completion(true) + } + case .animated: + let previousFrame = layer.frame + layer.frame = frame + self.animatePositionAdditive(layer: layer, offset: CGPoint(x: previousFrame.minX - frame.minX, y: previousFrame.minY - frame.minY)) + } + } + } + func updateFrameAdditive(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index f6ecae5ac1..96c8ec0eb8 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -476,6 +476,7 @@ public func generateSingleColorImage(size: CGSize, color: UIColor, scale: CGFloa public enum DrawingContextBltMode { case Alpha + case AlphaFromColor } public func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings { @@ -745,38 +746,64 @@ public class DrawingContext { let maxDstY = dstY + height switch mode { - case .Alpha: - while dstY < maxDstY { - let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self) - let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) + case .Alpha: + while dstY < maxDstY { + let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self) + let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) + + var dx = dstX + var sx = srcX + while dx < maxDstX { + let srcPixel = srcLine + sx + let dstPixel = dstLine + dx - var dx = dstX - var sx = srcX - while dx < maxDstX { - let srcPixel = srcLine + sx - let dstPixel = dstLine + dx - - let baseColor = dstPixel.pointee - let baseAlpha = (baseColor >> 24) & 0xff - let baseR = (baseColor >> 16) & 0xff - let baseG = (baseColor >> 8) & 0xff - let baseB = baseColor & 0xff - - let alpha = min(baseAlpha, srcPixel.pointee >> 24) - - let r = (baseR * alpha) / 255 - let g = (baseG * alpha) / 255 - let b = (baseB * alpha) / 255 - - dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b - - dx += 1 - sx += 1 - } + let baseColor = dstPixel.pointee + let baseAlpha = (baseColor >> 24) & 0xff + let baseR = (baseColor >> 16) & 0xff + let baseG = (baseColor >> 8) & 0xff + let baseB = baseColor & 0xff - dstY += 1 - srcY += 1 + let alpha = min(baseAlpha, srcPixel.pointee >> 24) + + let r = (baseR * alpha) / 255 + let g = (baseG * alpha) / 255 + let b = (baseB * alpha) / 255 + + dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b + + dx += 1 + sx += 1 } + + dstY += 1 + srcY += 1 + } + case .AlphaFromColor: + while dstY < maxDstY { + let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self) + let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) + + var dx = dstX + var sx = srcX + while dx < maxDstX { + let srcPixel = srcLine + sx + let dstPixel = dstLine + dx + + let alpha = (srcPixel.pointee >> 0) & 0xff + + let r = alpha + let g = alpha + let b = alpha + + dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b + + dx += 1 + sx += 1 + } + + dstY += 1 + srcY += 1 + } } } } diff --git a/submodules/Display/Source/ImmediateTextNode.swift b/submodules/Display/Source/ImmediateTextNode.swift index 7957d6fa9b..8851e6c209 100644 --- a/submodules/Display/Source/ImmediateTextNode.swift +++ b/submodules/Display/Source/ImmediateTextNode.swift @@ -277,6 +277,7 @@ open class ImmediateTextView: TextView { private var linkHighlightingNode: LinkHighlightingNode? public var linkHighlightColor: UIColor? + public var linkHighlightInset: UIEdgeInsets = .zero public var trailingLineWidth: CGFloat? @@ -356,7 +357,7 @@ open class ImmediateTextView: TextView { } } - if let rects = rects { + if var rects, !rects.isEmpty { let linkHighlightingNode: LinkHighlightingNode if let current = strongSelf.linkHighlightingNode { linkHighlightingNode = current @@ -366,7 +367,8 @@ open class ImmediateTextView: TextView { strongSelf.addSubnode(linkHighlightingNode) } linkHighlightingNode.frame = strongSelf.bounds - linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) }) + rects[rects.count - 1] = rects[rects.count - 1].inset(by: strongSelf.linkHighlightInset) + linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = strongSelf.linkHighlightingNode { strongSelf.linkHighlightingNode = nil linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index 252d5efc54..8072f48881 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -188,7 +188,7 @@ public final class TextAlertContentNode: AlertContentNode { } } - public init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout, dismissOnOutsideTap: Bool) { + public init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout, dismissOnOutsideTap: Bool, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) { self.theme = theme self.actionLayout = actionLayout self._dismissOnOutsideTap = dismissOnOutsideTap @@ -214,6 +214,15 @@ public final class TextAlertContentNode: AlertContentNode { self.textNode.isAccessibilityElement = true self.textNode.accessibilityLabel = text.string self.textNode.insets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + self.textNode.tapAttributeAction = linkAction + self.textNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + } + self.textNode.linkHighlightColor = theme.accentColor.withMultipliedAlpha(0.1) if text.length != 0 { if let paragraphStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { self.textNode.textAlignment = paragraphStyle.alignment @@ -450,7 +459,7 @@ public func textAlertController(theme: AlertControllerTheme, title: NSAttributed return AlertController(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title, text: text, actions: actions, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap)) } -public func standardTextAlertController(theme: AlertControllerTheme, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true) -> AlertController { +public func standardTextAlertController(theme: AlertControllerTheme, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { var dismissImpl: (() -> Void)? let attributedText: NSAttributedString if parseMarkdown { @@ -458,7 +467,10 @@ public func standardTextAlertController(theme: AlertControllerTheme, title: Stri let boldFont = title == nil ? Font.bold(theme.baseFontSize) : Font.semibold(floor(theme.baseFontSize * 13.0 / 17.0)) let body = MarkdownAttributeSet(font: font, textColor: theme.primaryColor) let bold = MarkdownAttributeSet(font: boldFont, textColor: theme.primaryColor) - attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center) + let link = MarkdownAttributeSet(font: font, textColor: theme.accentColor) + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { url in + return ("URL", url) + }), textAlignment: .center) } else { attributedText = NSAttributedString(string: text, font: title == nil ? Font.semibold(theme.baseFontSize) : Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)), textColor: theme.primaryColor, paragraphAlignment: .center) } @@ -467,7 +479,7 @@ public func standardTextAlertController(theme: AlertControllerTheme, title: Stri dismissImpl?() action.action() }) - }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap), allowInputInset: allowInputInset) + }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction), allowInputInset: allowInputInset) dismissImpl = { [weak controller] in controller?.dismissAnimated() } diff --git a/submodules/Display/Source/TooltipController.swift b/submodules/Display/Source/TooltipController.swift index 5ddaa0ac01..101f10a734 100644 --- a/submodules/Display/Source/TooltipController.swift +++ b/submodules/Display/Source/TooltipController.swift @@ -66,15 +66,23 @@ public enum SourceAndRect { case node(() -> (ASDisplayNode, CGRect)?) case view(() -> (UIView, CGRect)?) - func globalRect() -> CGRect? { + func globalRect(isAlreadyGlobal: Bool) -> CGRect? { switch self { case let .node(node): if let (sourceNode, sourceRect) = node() { - return sourceNode.view.convert(sourceRect, to: nil) + if isAlreadyGlobal { + return sourceRect + } else { + return sourceNode.view.convert(sourceRect, to: nil) + } } case let .view(view): if let (sourceView, sourceRect) = view() { - return sourceView.convert(sourceRect, to: nil) + if isAlreadyGlobal { + return sourceRect + } else { + return sourceView.convert(sourceRect, to: nil) + } } } return nil @@ -83,13 +91,16 @@ public enum SourceAndRect { public final class TooltipControllerPresentationArguments { public let sourceAndRect: SourceAndRect + public let sourceRectIsGlobal: Bool - public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect)?) { + public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect)?, sourceRectIsGlobal: Bool = false) { self.sourceAndRect = .node(sourceNodeAndRect) + self.sourceRectIsGlobal = sourceRectIsGlobal } - public init(sourceViewAndRect: @escaping () -> (UIView, CGRect)?) { + public init(sourceViewAndRect: @escaping () -> (UIView, CGRect)?, sourceRectIsGlobal: Bool = false) { self.sourceAndRect = .view(sourceViewAndRect) + self.sourceRectIsGlobal = sourceRectIsGlobal } } @@ -194,7 +205,7 @@ open class TooltipController: ViewController, StandalonePresentableController { } else { self.layout = layout - if let presentationArguments = self.presentationArguments as? TooltipControllerPresentationArguments, let sourceRect = presentationArguments.sourceAndRect.globalRect() { + if let presentationArguments = self.presentationArguments as? TooltipControllerPresentationArguments, let sourceRect = presentationArguments.sourceAndRect.globalRect(isAlreadyGlobal: presentationArguments.sourceRectIsGlobal) { self.controllerNode.sourceRect = sourceRect } else { self.controllerNode.sourceRect = nil diff --git a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift index 5ef068ce40..2084221c27 100644 --- a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift @@ -209,7 +209,7 @@ public final class DrawingLinkEntityView: DrawingEntityView, UITextViewDelegate if !self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { string = self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() } else { - string = self.linkEntity.url.uppercased().replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "tonsite://", with: "") + string = self.linkEntity.url.uppercased().replacingOccurrences(of: "HTTP://", with: "").replacingOccurrences(of: "HTTPS://", with: "").replacingOccurrences(of: "TONSITE://", with: "") } let text = NSMutableAttributedString(string: string) let range = NSMakeRange(0, text.length) diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFormatContext.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFormatContext.h index efcec50cb8..d4e53eaf6b 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFormatContext.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFormatContext.h @@ -44,6 +44,7 @@ extern int FFMpegCodecIdVP9; - (bool)isAttachedPicAtStreamIndex:(int32_t)streamIndex; - (int)codecIdAtStreamIndex:(int32_t)streamIndex; - (double)duration; +- (int64_t)startTimeAtStreamIndex:(int32_t)streamIndex; - (int64_t)durationAtStreamIndex:(int32_t)streamIndex; - (bool)codecParamsAtStreamIndex:(int32_t)streamIndex toContext:(FFMpegAVCodecContext *)context; - (FFMpegFpsAndTimebase)fpsAndTimebaseForStreamIndex:(int32_t)streamIndex defaultTimeBase:(CMTime)defaultTimeBase; diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h index 8f4f8314b1..4eb1d26df7 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVIOContext.h @@ -8,7 +8,7 @@ extern int FFMPEG_CONSTANT_AVERROR_EOF; @interface FFMpegAVIOContext : NSObject -- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable; +- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const _Nullable)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable; - (void *)impl; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m index bd4a655015..45dd51550e 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFormatContext.m @@ -103,6 +103,10 @@ int FFMpegCodecIdVP9 = AV_CODEC_ID_VP9; return (double)_impl->duration / AV_TIME_BASE; } +- (int64_t)startTimeAtStreamIndex:(int32_t)streamIndex { + return _impl->streams[streamIndex]->start_time; +} + - (int64_t)durationAtStreamIndex:(int32_t)streamIndex { return _impl->streams[streamIndex]->duration; } diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m b/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m index b0e54a15ac..a22e467c2b 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVIOContext.m @@ -12,7 +12,7 @@ int FFMPEG_CONSTANT_AVERROR_EOF = AVERROR_EOF; @implementation FFMpegAVIOContext -- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable { +- (instancetype _Nullable)initWithBufferSize:(int32_t)bufferSize opaqueContext:(void * const _Nullable)opaqueContext readPacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))readPacket writePacket:(int (* _Nullable)(void * _Nullable opaque, uint8_t * _Nullable buf, int buf_size))writePacket seek:(int64_t (*)(void * _Nullable opaque, int64_t offset, int whence))seek isSeekable:(bool)isSeekable { self = [super init]; if (self != nil) { void *avIoBuffer = av_malloc(bufferSize); diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index 40fc66c24f..1ebf1cad4c 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -45,7 +45,8 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", - "//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem", + "//submodules/TelegramUI/Components/SliderContextItem", + "//submodules/TelegramUI/Components/SectionTitleContextItem", "//submodules/TooltipUI", "//submodules/TelegramNotices", "//submodules/Pasteboard", @@ -53,6 +54,15 @@ swift_library( "//submodules/TelegramUI/Components/Ads/AdsInfoScreen", "//submodules/TelegramUI/Components/Ads/AdsReportScreen", "//submodules/UrlHandling", + "//submodules/TelegramUI/Components/SaveProgressScreen", + "//submodules/TelegramUI/Components/RasterizedCompositionComponent", + "//submodules/TelegramUI/Components/BadgeComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/ComponentFlow", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 082471eebc..ce9f9e7b01 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -957,7 +957,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll } else if media is TelegramMediaImage { hasCaption = true } else if let file = media as? TelegramMediaFile { - hasCaption = file.mimeType.hasPrefix("image/") + hasCaption = file.mimeType.hasPrefix("image/") || file.mimeType.hasPrefix("video/") } else if media is TelegramMediaInvoice { hasCaption = true } diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index b141da652a..3e2b6ce31d 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -245,7 +245,18 @@ public func galleryItemForEntry( content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), loopVideo: true, enableSound: false, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) } else { if true || (file.mimeType == "video/mpeg4" || file.mimeType == "video/mov" || file.mimeType == "video/mp4") { + var isHLS = false if NativeVideoContent.isHLSVideo(file: file) { + isHLS = true + + if let data = context.currentAppConfiguration.with({ $0 }).data, let disableHLS = data["video_ignore_alt_documents"] as? Double { + if Int(disableHLS) != 0 { + isHLS = false + } + } + } + + if isHLS { content = HLSVideoContent(id: .message(message.id, message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), streamVideo: streamVideos, loopVideo: loopVideos) } else { content = NativeVideoContent(id: .message(message.stableId, file.fileId), userLocation: .peer(message.id.peerId), fileReference: .message(message: MessageReference(message), media: file), imageReference: mediaImage.flatMap({ ImageMediaReference.message(message: MessageReference(message), media: $0) }), streamVideo: .conservative, loopVideo: loopVideos, tempFilePath: tempFilePath, captureProtected: captureProtected, storeAfterDownload: generateStoreAfterDownload?(message, file)) @@ -309,13 +320,16 @@ public func galleryItemForEntry( } else { if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" { return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location) - } - else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { + } else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { var pixelsCount: Int = 0 if let dimensions = file.dimensions { pixelsCount = Int(dimensions.width) * Int(dimensions.height) } - if pixelsCount < 10000 * 10000 { + var fileSize: Int64 = 0 + if let size = file.size { + fileSize = size + } + if pixelsCount < 10000 * 10000 && fileSize < 16 * 1024 * 1024 { return ChatImageGalleryItem( context: context, presentationData: presentationData, @@ -697,13 +711,8 @@ public class GalleryController: ViewController, StandalonePresentableController, translateToLanguage = chatTranslationState(context: context, peerId: messageId.peerId) |> map { translationState in if let translationState, translationState.isEnabled { - var translateToLanguage = translationState.toLang ?? baseLanguageCode - if translateToLanguage == "nb" { - translateToLanguage = "no" - } else if translateToLanguage == "pt-br" { - translateToLanguage = "pt" - } - return translateToLanguage + let translateToLanguage = translationState.toLang ?? baseLanguageCode + return normalizeTranslationLanguage(translateToLanguage) } else { return nil } diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 84cccba91c..d269eddc5e 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -303,7 +303,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture self.pager.frame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size) - self.pager.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + self.pager.containerLayoutUpdated(layout, navigationBarHeight: self.areControlsHidden ? 0.0 : navigationBarHeight, transition: transition) } open func setControlsHidden(_ hidden: Bool, animated: Bool) { @@ -470,6 +470,10 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture let distanceFromEquilibrium = scrollView.contentOffset.y - scrollView.contentSize.height / 3.0 let minimalDismissDistance = scrollView.contentSize.height / 12.0 if abs(velocity.y) > 1.0 || abs(distanceFromEquilibrium) > minimalDismissDistance { + if distanceFromEquilibrium > 1.0, let centralItemNode = self.pager.centralItemNode(), centralItemNode.maybePerformActionForSwipeDismiss() { + return + } + if let backgroundColor = self.backgroundNode.backgroundColor { self.backgroundNode.layer.animate(from: backgroundColor, to: UIColor(white: 0.0, alpha: 0.0).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2, removeOnCompletion: false) } diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 222a018862..1e3d02dbf1 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -11,6 +11,11 @@ public enum GalleryItemNodeNavigationStyle { } open class GalleryItemNode: ASDisplayNode { + public enum ActiveEdge { + case left + case right + } + private var _index: Int? public var index: Int { get { @@ -100,6 +105,10 @@ open class GalleryItemNode: ASDisplayNode { open func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { } + open func maybePerformActionForSwipeDismiss() -> Bool { + return false + } + open func contentSize() -> CGSize? { return nil } @@ -107,4 +116,14 @@ open class GalleryItemNode: ASDisplayNode { open var keyShortcuts: [KeyShortcut] { return [] } + + open func hasActiveEdgeAction(edge: ActiveEdge) -> Bool { + return false + } + + open func setActiveEdgeAction(edge: ActiveEdge?) { + } + + open func adjustActiveEdgeAction(distance: CGFloat) { + } } diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 59bfe42de1..01d821e7c5 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -9,6 +9,10 @@ private func edgeWidth(width: CGFloat) -> CGFloat { return min(44.0, floor(width / 6.0)) } +private func activeEdgeWidth(width: CGFloat) -> CGFloat { + return floor(width * 0.4) +} + let fadeWidth: CGFloat = 70.0 private let leftFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -85,6 +89,9 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest private let leftFadeNode: ASDisplayNode private let rightFadeNode: ASDisplayNode private var highlightedSide: Bool? + private var activeSide: Bool? + private var canPerformSideNavigationAction: Bool = false + private var sideActionInitialPosition: CGPoint? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? @@ -118,6 +125,8 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest public var pagingEnabledPromise = Promise(true) private var pagingEnabledDisposable: Disposable? + private var edgeLongTapTimer: Foundation.Timer? + public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap self.disableTapNavigation = disableTapNavigation @@ -170,57 +179,99 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest recognizer.delegate = self.wrappedGestureRecognizerDelegate self.tapRecognizer = recognizer recognizer.tapActionAtPoint = { [weak self] point in - guard let strongSelf = self, strongSelf.pagingEnabled else { + guard let strongSelf = self else { return .fail } let size = strongSelf.bounds var highlightedSide: Bool? - if point.x < edgeWidth(width: size.width) && strongSelf.canGoToPreviousItem() { - if strongSelf.items.count > 1 { - highlightedSide = false + var activeSide: Bool? + if point.x < edgeWidth(width: size.width) { + if strongSelf.canGoToPreviousItem() { + if strongSelf.items.count > 1 { + highlightedSide = false + } } - } else if point.x > size.width - edgeWidth(width: size.width) && strongSelf.canGoToNextItem() { - if strongSelf.items.count > 1 { - if point.y < 80.0 { - highlightedSide = nil - } else { + } else if point.x > size.width - edgeWidth(width: size.width) { + if strongSelf.canGoToNextItem() { + if strongSelf.items.count > 1 { highlightedSide = true } } } + if point.x < activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) { + activeSide = false + } + } else if point.x > size.width - activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) { + activeSide = true + } + } - if highlightedSide == nil { + if !strongSelf.pagingEnabled { + highlightedSide = nil + } + + if highlightedSide == nil && activeSide == nil { return .fail } if let result = strongSelf.hitTest(point, with: nil), let _ = result.asyncdisplaykit_node as? ASButtonNode { return .fail } - return .keepWithSingleTap + + if activeSide != nil { + return .waitForHold(timeout: 0.3, acceptTap: true) + } else { + return .keepWithSingleTap + } } recognizer.highlight = { [weak self] point in - guard let strongSelf = self, strongSelf.pagingEnabled else { + guard let strongSelf = self else { return } let size = strongSelf.bounds var highlightedSide: Bool? - if let point = point { - if point.x < edgeWidth(width: size.width) && strongSelf.canGoToPreviousItem() { - if strongSelf.items.count > 1 { - highlightedSide = false + var activeSide: Bool? + if let point { + if point.x < edgeWidth(width: size.width) { + if strongSelf.canGoToPreviousItem() { + if strongSelf.items.count > 1 { + highlightedSide = false + } } - } else if point.x > size.width - edgeWidth(width: size.width) && strongSelf.canGoToNextItem() { - if strongSelf.items.count > 1 { - highlightedSide = true + } else if point.x > size.width - edgeWidth(width: size.width) { + if strongSelf.canGoToNextItem() { + if strongSelf.items.count > 1 { + highlightedSide = true + } + } + } + if point.x < activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .left) { + activeSide = false + } + } else if point.x > size.width - activeEdgeWidth(width: size.width) { + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex), itemNode.hasActiveEdgeAction(edge: .right) { + activeSide = true } } } + + if !strongSelf.pagingEnabled { + highlightedSide = nil + } + if strongSelf.highlightedSide != highlightedSide { strongSelf.highlightedSide = highlightedSide + if highlightedSide != nil { + strongSelf.canPerformSideNavigationAction = true + } + let leftAlpha: CGFloat let rightAlpha: CGFloat if let highlightedSide = highlightedSide { @@ -247,6 +298,47 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest } } } + + if strongSelf.activeSide != activeSide { + strongSelf.activeSide = activeSide + + if let activeSide, let centralIndex = strongSelf.centralItemIndex, let _ = strongSelf.visibleItemNode(at: centralIndex) { + if strongSelf.edgeLongTapTimer == nil { + strongSelf.edgeLongTapTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false, block: { _ in + guard let self else { + return + } + if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) { + itemNode.setActiveEdgeAction(edge: activeSide ? .right : .left) + } + + self.canPerformSideNavigationAction = false + + let leftAlpha: CGFloat + let rightAlpha: CGFloat + + leftAlpha = 0.0 + rightAlpha = 0.0 + + if self.leftFadeNode.alpha != leftAlpha { + self.leftFadeNode.alpha = leftAlpha + self.leftFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) + } + if self.rightFadeNode.alpha != rightAlpha { + self.rightFadeNode.alpha = rightAlpha + self.rightFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) + } + }) + } + } else if let edgeLongTapTimer = strongSelf.edgeLongTapTimer { + edgeLongTapTimer.invalidate() + strongSelf.edgeLongTapTimer = nil + + if let centralIndex = strongSelf.centralItemIndex, let itemNode = strongSelf.visibleItemNode(at: centralIndex) { + itemNode.setActiveEdgeAction(edge: nil) + } + } + } } self.view.addGestureRecognizer(recognizer) } @@ -258,8 +350,9 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: + self.sideActionInitialPosition = nil if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { - if case .tap = gesture { + if case .tap = gesture, self.canPerformSideNavigationAction { let size = self.bounds.size if location.x < edgeWidth(width: size.width) && self.canGoToPreviousItem() { self.goToPreviousItem() @@ -268,6 +361,21 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest } } } + case .cancelled: + self.sideActionInitialPosition = nil + case .began: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { + self.sideActionInitialPosition = location + } + case .changed: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation, case .hold = gesture { + if let sideActionInitialPosition = self.sideActionInitialPosition { + let distance = location.x - sideActionInitialPosition.x + if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) { + itemNode.adjustActiveEdgeAction(distance: distance) + } + } + } default: break } diff --git a/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift b/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift new file mode 100644 index 0000000000..e596760fc2 --- /dev/null +++ b/submodules/GalleryUI/Sources/GalleryRateToastAnimationComponent.swift @@ -0,0 +1,118 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle + +final class GalleryRateToastAnimationComponent: Component { + let speedFraction: CGFloat + + init(speedFraction: CGFloat) { + self.speedFraction = speedFraction + } + + static func ==(lhs: GalleryRateToastAnimationComponent, rhs: GalleryRateToastAnimationComponent) -> Bool { + if lhs.speedFraction != rhs.speedFraction { + return false + } + return true + } + + final class View: UIView { + private let itemViewContainer: UIView + private var itemViews: [UIImageView] = [] + + private var link: SharedDisplayLinkDriver.Link? + private var timeValue: CGFloat = 0.0 + private var speedFraction: CGFloat = 1.0 + + override init(frame: CGRect) { + self.itemViewContainer = UIView() + + super.init(frame: frame) + + self.addSubview(self.itemViewContainer) + + let image = UIImage(bundleImageName: "Media Gallery/VideoRateToast")?.withRenderingMode(.alwaysTemplate) + for _ in 0 ..< 2 { + let itemView = UIImageView(image: image) + itemView.tintColor = .white + self.itemViews.append(itemView) + self.itemViewContainer.addSubview(itemView) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.link?.invalidate() + } + + private func setupAnimations() { + if self.link == nil { + var previousTimestamp = CACurrentMediaTime() + self.link = SharedDisplayLinkDriver.shared.add { [weak self] _ in + guard let self else { + return + } + + let timestamp = CACurrentMediaTime() + let deltaMultiplier = 1.0 * (1.0 - self.speedFraction) + 3.0 * self.speedFraction + let deltaTime = (timestamp - previousTimestamp) * deltaMultiplier + previousTimestamp = timestamp + + self.timeValue += deltaTime + + let duration: CGFloat = 1.2 + + for i in 0 ..< self.itemViews.count { + var itemFraction = (self.timeValue + CGFloat(i) * 0.1).truncatingRemainder(dividingBy: duration) / duration + + if itemFraction >= 0.5 { + itemFraction = (1.0 - itemFraction) / 0.5 + } else { + itemFraction = itemFraction / 0.5 + } + + let itemAlpha = 0.6 * (1.0 - itemFraction) + 1.0 * itemFraction + let itemScale = 0.9 * (1.0 - itemFraction) + 1.1 * itemFraction + + self.itemViews[i].alpha = itemAlpha + self.itemViews[i].transform = CGAffineTransformMakeScale(itemScale, itemScale) + } + } + } + } + + func update(component: GalleryRateToastAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.speedFraction = component.speedFraction + + let itemSize = self.itemViews[0].image?.size ?? CGSize(width: 10.0, height: 10.0) + let itemSpacing: CGFloat = 1.0 + + let size = CGSize(width: itemSize.width * 2.0 + itemSpacing, height: 12.0) + + for i in 0 ..< self.itemViews.count { + let itemFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (itemSize.width + itemSpacing), y: UIScreenPixel), size: itemSize) + self.itemViews[i].center = itemFrame.center + self.itemViews[i].bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + + self.itemViews[i].layer.speed = Float(1.0 * (1.0 - component.speedFraction) + 2.0 * component.speedFraction) + } + + self.setupAnimations() + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift b/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift new file mode 100644 index 0000000000..caadc1e407 --- /dev/null +++ b/submodules/GalleryUI/Sources/GalleryRateToastComponent.swift @@ -0,0 +1,234 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle +import BalancedTextComponent +import AnimatedTextComponent +import LottieComponent + +final class GalleryRateToastComponent: Component { + let rate: Double + let displayTooltip: String? + + init(rate: Double, displayTooltip: String?) { + self.rate = rate + self.displayTooltip = displayTooltip + } + + static func ==(lhs: GalleryRateToastComponent, rhs: GalleryRateToastComponent) -> Bool { + if lhs.rate != rhs.rate { + return false + } + if lhs.displayTooltip != rhs.displayTooltip { + return false + } + return true + } + + final class View: UIView { + private let background = ComponentView() + private let text = ComponentView() + private let arrows = ComponentView() + + private var tooltipText: ComponentView? + private var tooltipAnimation: ComponentView? + + private var tooltipIsHidden: Bool = false + private var tooltipTimer: Foundation.Timer? + + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.tooltipTimer?.invalidate() + } + + func update(component: GalleryRateToastComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.state = state + + let insets = UIEdgeInsets(top: 5.0, left: 11.0, bottom: 5.0, right: 16.0) + let spacing: CGFloat = 5.0 + + var rateString = String(format: "%.1f", component.rate) + if rateString.hasSuffix(".0") { + rateString = rateString.replacingOccurrences(of: ".0", with: "") + } + + var textItems: [AnimatedTextComponent.Item] = [] + if let dotRange = rateString.range(of: ".") { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("pre"), content: .text(String(rateString[rateString.startIndex ..< dotRange.lowerBound])))) + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("dot"), content: .text("."))) + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("post"), content: .text(String(rateString[dotRange.upperBound...])))) + } else { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("pre"), content: .text(rateString))) + } + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("x"), content: .text("x"))) + + let textSize = self.text.update( + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 17.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), + color: .white, + items: textItems + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + var speedFraction = (component.rate - 1.0) / (2.5 - 1.0) + speedFraction = max(0.0, min(1.0, speedFraction)) + let arrowsSize = self.arrows.update( + transition: transition, + component: AnyComponent(GalleryRateToastAnimationComponent(speedFraction: speedFraction)), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + + let size = CGSize(width: insets.left + insets.right + textSize.width + arrowsSize.width, height: insets.top + insets.bottom + max(textSize.height, arrowsSize.height)) + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: UIColor(white: 0.0, alpha: 0.5), + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + let backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - size.width) * 0.5), y: 0.0), size: size) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + insets.left, y: backgroundFrame.minY + floorToScreenPixels((size.height - textSize.height) * 0.5)), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + textView.layer.anchorPoint = CGPoint() + self.addSubview(textView) + } + transition.setPosition(view: textView, position: textFrame.origin) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + let arrowsFrame = CGRect(origin: CGPoint(x: textFrame.maxX + spacing, y: backgroundFrame.minY + floorToScreenPixels((size.height - arrowsSize.height) * 0.5)), size: arrowsSize) + if let arrowsView = self.arrows.view { + if arrowsView.superview == nil { + self.addSubview(arrowsView) + } + transition.setFrame(view: arrowsView, frame: arrowsFrame) + } + + if let displayTooltip = component.displayTooltip { + var tooltipTransition = transition + + let tooltipText: ComponentView + if let current = self.tooltipText { + tooltipText = current + } else { + tooltipText = ComponentView() + self.tooltipText = tooltipText + tooltipTransition = tooltipTransition.withAnimation(.none) + } + + let tooltipAnimation: ComponentView + if let current = self.tooltipAnimation { + tooltipAnimation = current + } else { + tooltipAnimation = ComponentView() + self.tooltipAnimation = tooltipAnimation + } + + let tooltipTextSize = tooltipText.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(NSAttributedString(string: displayTooltip, font: Font.regular(15.0), textColor: UIColor(white: 1.0, alpha: 0.8))), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 1000.0) + ) + let tooltipTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - tooltipTextSize.width) * 0.5), y: backgroundFrame.maxY + 10.0), size: tooltipTextSize) + if let tooltipTextView = tooltipText.view { + if tooltipTextView.superview == nil { + self.addSubview(tooltipTextView) + } + tooltipTransition.setPosition(view: tooltipTextView, position: tooltipTextFrame.center) + tooltipTextView.bounds = CGRect(origin: CGPoint(), size: tooltipTextFrame.size) + + transition.setAlpha(view: tooltipTextView, alpha: self.tooltipIsHidden ? 0.0 : 1.0) + } + + let tooltipAnimationSize = tooltipAnimation.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "video_toast_speedup"), + color: .white, + startingPosition: .begin, + loop: true + )), + environment: {}, + containerSize: CGSize(width: 60.0, height: 60.0) + ) + let tooltipAnimationFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - tooltipAnimationSize.width) * 0.5), y: tooltipTextFrame.maxY + 10.0), size: tooltipAnimationSize) + if let tooltipAnimationView = tooltipAnimation.view { + if tooltipAnimationView.superview == nil { + self.addSubview(tooltipAnimationView) + } + tooltipTransition.setFrame(view: tooltipAnimationView, frame: tooltipAnimationFrame) + + transition.setAlpha(view: tooltipAnimationView, alpha: self.tooltipIsHidden ? 0.0 : 0.8) + } + } else { + if let tooltipText = self.tooltipText { + self.tooltipText = nil + if let tooltipTextView = tooltipText.view { + transition.setAlpha(view: tooltipTextView, alpha: 0.0, completion: { [weak tooltipTextView] _ in + tooltipTextView?.removeFromSuperview() + }) + } + } + if let tooltipAnimation = self.tooltipAnimation { + self.tooltipAnimation = nil + if let tooltipAnimationView = tooltipAnimation.view { + transition.setAlpha(view: tooltipAnimationView, alpha: 0.0, completion: { [weak tooltipAnimationView] _ in + tooltipAnimationView?.removeFromSuperview() + }) + } + } + } + + if self.tooltipTimer == nil { + self.tooltipTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.tooltipIsHidden = true + self.state?.updated(transition: .easeInOut(duration: 0.25), isLocal: true) + }) + } + + return CGSize(width: availableSize.width, height: size.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index 3085831148..0ea96bd194 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -552,7 +552,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { }, iconSource: nil, action: { [weak self] _, f in f(.dismissWithoutContent) if let navigationController = self?.baseNavigationController() as? NavigationController { - navigationController.pushViewController(AdsInfoScreen(context: context)) + navigationController.pushViewController(AdsInfoScreen(context: context, mode: .channel)) } }))) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 09df0e7fc8..f3a46f5fbc 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -27,6 +27,12 @@ import Pasteboard import AdUI import AdsInfoScreen import AdsReportScreen +import SaveProgressScreen +import SectionTitleContextItem +import RasterizedCompositionComponent +import BadgeComponent +import ComponentFlow +import ComponentDisplayAdapters public enum UniversalVideoGalleryItemContentInfo { case message(Message, Int?) @@ -502,6 +508,228 @@ final class MoreHeaderButton: HighlightableButtonNode { } } +final class SettingsHeaderButton: HighlightableButtonNode { + let referenceNode: ContextReferenceContentNode + let containerNode: ContextControllerSourceNode + + private let iconLayer: RasterizedCompositionMonochromeLayer + + private let gearsLayer: RasterizedCompositionImageLayer + private let dotLayer: RasterizedCompositionImageLayer + + private var speedBadge: ComponentView? + private var qualityBadge: ComponentView? + + private var speedBadgeText: String? + private var qualityBadgeText: String? + + private let badgeFont: UIFont + + private var isMenuOpen: Bool = false + + var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + + private let wide: Bool + + init(wide: Bool = false) { + self.wide = wide + + self.referenceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + + self.iconLayer = RasterizedCompositionMonochromeLayer() + //self.iconLayer.backgroundColor = UIColor.green.cgColor + + self.gearsLayer = RasterizedCompositionImageLayer() + self.gearsLayer.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsNoDot"), color: .white) + + self.dotLayer = RasterizedCompositionImageLayer() + self.dotLayer.image = generateFilledCircleImage(diameter: 4.0, color: .white) + + self.iconLayer.contentsLayer.addSublayer(self.gearsLayer) + self.iconLayer.contentsLayer.addSublayer(self.dotLayer) + + self.badgeFont = Font.with(size: 8.0, design: .round, weight: .bold) + + super.init() + + self.containerNode.addSubnode(self.referenceNode) + self.referenceNode.layer.addSublayer(self.iconLayer) + self.addSubnode(self.containerNode) + + self.containerNode.shouldBegin = { [weak self] location in + guard let strongSelf = self, let _ = strongSelf.contextAction else { + return false + } + return true + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self else { + return + } + strongSelf.contextAction?(strongSelf.containerNode, gesture) + } + + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0)) + self.referenceNode.frame = self.containerNode.bounds + + self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0) + + if let image = self.gearsLayer.image { + let iconInnerInsets = UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 6.0) + let iconSize = CGSize(width: image.size.width + iconInnerInsets.left + iconInnerInsets.right, height: image.size.height + iconInnerInsets.top + iconInnerInsets.bottom) + let iconFrame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - iconSize.width) / 2.0), y: floor((self.containerNode.bounds.height - iconSize.height) / 2.0)), size: iconSize) + self.iconLayer.position = iconFrame.center + self.iconLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconLayer.contentsLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center + self.iconLayer.contentsLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + + self.iconLayer.maskedLayer.position = CGRect(origin: CGPoint(), size: iconFrame.size).center + self.iconLayer.maskedLayer.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) + self.iconLayer.maskedLayer.backgroundColor = UIColor.white.cgColor + + let gearsFrame = CGRect(origin: CGPoint(x: floor((iconSize.width - image.size.width) * 0.5), y: floor((iconSize.height - image.size.height) * 0.5)), size: image.size) + self.gearsLayer.position = gearsFrame.center + self.gearsLayer.bounds = CGRect(origin: CGPoint(), size: gearsFrame.size) + + if let dotImage = self.dotLayer.image { + let dotFrame = CGRect(origin: CGPoint(x: gearsFrame.minX + floorToScreenPixels((gearsFrame.width - dotImage.size.width) * 0.5), y: gearsFrame.minY + floorToScreenPixels((gearsFrame.height - dotImage.size.height) * 0.5)), size: dotImage.size) + self.dotLayer.position = dotFrame.center + self.dotLayer.bounds = CGRect(origin: CGPoint(), size: dotFrame.size) + } + } + } + + override func didLoad() { + super.didLoad() + self.view.isOpaque = false + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: wide ? 32.0 : 22.0, height: 44.0) + } + + func onLayout() { + } + + func setIsMenuOpen(isMenuOpen: Bool) { + if self.isMenuOpen == isMenuOpen { + return + } + self.isMenuOpen = isMenuOpen + + let rotationTransition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + rotationTransition.updateTransform(layer: self.gearsLayer, transform: CGAffineTransformMakeRotation(isMenuOpen ? (CGFloat.pi * 2.0 / 6.0) : 0.0)) + self.gearsLayer.animateScale(from: 1.0, to: 1.07, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.gearsLayer.animateScale(from: 1.07, to: 1.0, duration: 0.1, removeOnCompletion: true) + }) + + self.dotLayer.animateScale(from: 1.0, to: 0.8, duration: 0.1, removeOnCompletion: false, completion: { [weak self] finished in + guard let self, finished else { + return + } + self.dotLayer.animateScale(from: 0.8, to: 1.0, duration: 0.1, removeOnCompletion: true) + }) + } + + func setBadges(speed: String?, quality: String?, transition: ComponentTransition) { + if self.speedBadgeText == speed && self.qualityBadgeText == quality { + return + } + self.speedBadgeText = speed + self.qualityBadgeText = quality + + if let badgeText = speed { + var badgeTransition = transition + let speedBadge: ComponentView + if let current = self.speedBadge { + speedBadge = current + } else { + speedBadge = ComponentView() + self.speedBadge = speedBadge + badgeTransition = badgeTransition.withAnimation(.none) + } + let badgeSize = speedBadge.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + text: badgeText, + font: self.badgeFont, + cornerRadius: 3.0, + insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), + outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let speedBadgeView = speedBadge.view { + if speedBadgeView.layer.superlayer == nil { + self.iconLayer.contentsLayer.addSublayer(speedBadgeView.layer) + + transition.animateAlpha(layer: speedBadgeView.layer, from: 0.0, to: 1.0) + transition.animateScale(layer: speedBadgeView.layer, from: 0.001, to: 1.0) + } + badgeTransition.setFrame(layer: speedBadgeView.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeSize)) + } + } else if let speedBadge = self.speedBadge { + self.speedBadge = nil + if let speedBadgeView = speedBadge.view { + let speedBadgeLayer = speedBadgeView.layer + transition.setAlpha(layer: speedBadgeLayer, alpha: 0.0, completion: { [weak speedBadgeLayer] _ in + speedBadgeLayer?.removeFromSuperlayer() + }) + transition.setScale(layer: speedBadgeLayer, scale: 0.001) + } + } + + if let badgeText = quality { + var badgeTransition = transition + let qualityBadge: ComponentView + if let current = self.qualityBadge { + qualityBadge = current + } else { + qualityBadge = ComponentView() + self.qualityBadge = qualityBadge + badgeTransition = badgeTransition.withAnimation(.none) + } + let badgeSize = qualityBadge.update( + transition: badgeTransition, + component: AnyComponent(BadgeComponent( + text: badgeText, + font: self.badgeFont, + cornerRadius: 3.0, + insets: UIEdgeInsets(top: 1.33, left: 1.66, bottom: 1.33, right: 1.66), + outerInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + if let qualityBadgeView = qualityBadge.view { + if qualityBadgeView.layer.superlayer == nil { + self.iconLayer.contentsLayer.addSublayer(qualityBadgeView.layer) + + transition.animateAlpha(layer: qualityBadgeView.layer, from: 0.0, to: 1.0) + transition.animateScale(layer: qualityBadgeView.layer, from: 0.001, to: 1.0) + } + badgeTransition.setFrame(layer: qualityBadgeView.layer, frame: CGRect(origin: CGPoint(x: self.iconLayer.bounds.width - badgeSize.width, y: self.iconLayer.bounds.height - badgeSize.height), size: badgeSize)) + } + } else if let qualityBadge = self.qualityBadge { + self.qualityBadge = nil + if let qualityBadgeView = qualityBadge.view { + let qualityBadgeLayer = qualityBadgeView.layer + transition.setAlpha(layer: qualityBadgeLayer, alpha: 0.0, completion: { [weak qualityBadgeLayer] _ in + qualityBadgeLayer?.removeFromSuperlayer() + }) + transition.setScale(layer: qualityBadgeLayer, scale: 0.001) + } + } + } +} + @available(iOS 15.0, *) private final class PictureInPictureContentImpl: NSObject, PictureInPictureContent, AVPictureInPictureControllerDelegate { private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { @@ -901,6 +1129,7 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict private let node: UniversalVideoNode private let willBegin: (NativePictureInPictureContentImpl) -> Void private let didBegin: (NativePictureInPictureContentImpl) -> Void + private let didEnd: (NativePictureInPictureContentImpl) -> Void private let expand: (@escaping () -> Void) -> Void private var pictureInPictureTimer: SwiftSignalKit.Timer? private var didExpand: Bool = false @@ -911,7 +1140,7 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict private var isNativePictureInPictureActiveDisposable: Disposable? - init(context: AccountContext, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didBegin: @escaping (NativePictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) { + init(context: AccountContext, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didEnd: @escaping (NativePictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) { self.context = context self.mediaManager = mediaManager self.accountId = accountId @@ -919,6 +1148,7 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict self.node = videoNode self.willBegin = willBegin self.didBegin = didBegin + self.didEnd = didEnd self.expand = expand super.init() @@ -938,21 +1168,30 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict } if let (messageId, _) = hiddenMedia { + var hadMessage: Bool? self.messageRemovedDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) |> map { message -> Bool in if let _ = message { - return false - } else { return true + } else { + return false } } - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let strongSelf = self else { + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { return } - strongSelf.node.canAttachContent = false + if let hadMessage, hadMessage { + if value { + } else { + if let pictureInPictureController = self.pictureInPictureController { + pictureInPictureController.stopPictureInPicture() + } + } + + return + } + hadMessage = value }) } } @@ -1028,6 +1267,7 @@ private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPict mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex) self.hiddenMediaManagerIndex = nil } + self.didEnd(self) } public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { @@ -1061,7 +1301,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var moreBarButtonRate: Double = 1.0 private var moreBarButtonRateTimestamp: Double? - private let settingsBarButton: MoreHeaderButton + private let settingsBarButton: SettingsHeaderButton private var videoNode: UniversalVideoNode? private var videoNodeUserInteractionEnabled: Bool = false @@ -1079,7 +1319,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var playOnContentOwnership = false private var skipInitialPause = false private var ignorePauseStatus = false - private var validLayout: (ContainerViewLayout, CGFloat)? + private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)? private var didPause = false private var isPaused = true private var dismissOnOrientationChange = false @@ -1094,9 +1334,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var playbackRate: Double? private var videoQuality: UniversalVideoContentVideoQuality = .auto private let playbackRatePromise = ValuePromise() + private let videoQualityPromise = ValuePromise() private let statusDisposable = MetaDisposable() private let moreButtonStateDisposable = MetaDisposable() + private let settingsButtonStateDisposable = MetaDisposable() private let mediaPlaybackStateDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() @@ -1112,6 +1354,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let isInteractingPromise = ValuePromise(false, ignoreRepeated: true) private let controlsVisiblePromise = ValuePromise(true, ignoreRepeated: true) private let isShowingContextMenuPromise = ValuePromise(false, ignoreRepeated: true) + private let isShowingSettingsMenuPromise = ValuePromise(false, ignoreRepeated: true) private let hasExpandedCaptionPromise = Promise() private var hideControlsDisposable: Disposable? private var automaticPictureInPictureDisposable: Disposable? @@ -1126,6 +1369,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var activePictureInPictureNavigationController: NavigationController? private var activePictureInPictureController: ViewController? + private var activeEdgeRateState: (initialRate: Double, currentRate: Double)? + private var activeEdgeRateIndicator: ComponentView? + init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.presentationData = presentationData @@ -1149,7 +1395,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.moreBarButton.isUserInteractionEnabled = true self.moreBarButton.setContent(.more(optionsCircleImage(dark: false))) - self.settingsBarButton = MoreHeaderButton() + self.settingsBarButton = SettingsHeaderButton() self.settingsBarButton.isUserInteractionEnabled = true super.init() @@ -1281,9 +1527,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.titleContentView = GalleryTitleView(frame: CGRect()) self._titleView.set(.single(self.titleContentView)) - let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.hasExpandedCaptionPromise.get()) - |> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, hasExpandedCaptionPromise -> Signal in - if isShowingContextMenu || hasExpandedCaptionPromise { + let shouldHideControlsSignal: Signal = combineLatest(self.isPlayingPromise.get(), self.isInteractingPromise.get(), self.controlsVisiblePromise.get(), self.isShowingContextMenuPromise.get(), self.isShowingSettingsMenuPromise.get(), self.hasExpandedCaptionPromise.get()) + |> mapToSignal { isPlaying, isInteracting, controlsVisible, isShowingContextMenu, isShowingSettingsMenu, hasExpandedCaptionPromise -> Signal in + if isShowingContextMenu || isShowingSettingsMenu || hasExpandedCaptionPromise { return .complete() } if isPlaying && !isInteracting && controlsVisible { @@ -1305,6 +1551,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { deinit { self.statusDisposable.dispose() self.moreButtonStateDisposable.dispose() + self.settingsButtonStateDisposable.dispose() self.mediaPlaybackStateDisposable.dispose() self.scrubbingFrameDisposable?.dispose() self.hideControlsDisposable?.dispose() @@ -1360,6 +1607,47 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { pictureInPictureNode.updateLayout(placeholderSize, transition: transition) } } + + if let activeEdgeRateState = self.activeEdgeRateState { + var activeEdgeRateIndicatorTransition = transition + let activeEdgeRateIndicator: ComponentView + if let current = self.activeEdgeRateIndicator { + activeEdgeRateIndicator = current + } else { + activeEdgeRateIndicator = ComponentView() + self.activeEdgeRateIndicator = activeEdgeRateIndicator + activeEdgeRateIndicatorTransition = .immediate + } + + let activeEdgeRateIndicatorSize = activeEdgeRateIndicator.update( + transition: ComponentTransition(activeEdgeRateIndicatorTransition), + component: AnyComponent(GalleryRateToastComponent( + rate: activeEdgeRateState.currentRate, + displayTooltip: self.presentationData.strings.Gallery_ToastVideoSpeedSwipe + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: 100.0) + ) + let activeEdgeRateIndicatorFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - activeEdgeRateIndicatorSize.width) * 0.5), y: max(navigationBarHeight, layout.statusBarHeight ?? 0.0) + 8.0), size: activeEdgeRateIndicatorSize) + if let activeEdgeRateIndicatorView = activeEdgeRateIndicator.view { + if activeEdgeRateIndicatorView.superview == nil { + self.view.addSubview(activeEdgeRateIndicatorView) + transition.animateTransformScale(view: activeEdgeRateIndicatorView, from: 0.001) + if transition.isAnimated { + activeEdgeRateIndicatorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + activeEdgeRateIndicatorTransition.updateFrame(view: activeEdgeRateIndicatorView, frame: activeEdgeRateIndicatorFrame) + } + } else if let activeEdgeRateIndicator = self.activeEdgeRateIndicator { + self.activeEdgeRateIndicator = nil + if let activeEdgeRateIndicatorView = activeEdgeRateIndicator.view { + transition.updateAlpha(layer: activeEdgeRateIndicatorView.layer, alpha: 0.0, completion: { [weak activeEdgeRateIndicatorView] _ in + activeEdgeRateIndicatorView?.removeFromSuperview() + }) + transition.updateTransformScale(layer: activeEdgeRateIndicatorView.layer, scale: 0.001) + } + } if dismiss { self.dismiss() @@ -1452,11 +1740,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { isAdaptive = true } - if isAdaptive { - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white))) - } else { - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettings"), color: .white))) - } + let _ = isAdaptive let dimensions = item.content.dimensions if dimensions.height > 0.0 { @@ -1597,46 +1881,48 @@ 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 - guard let strongSelf = self else { + self.videoQualityPromise.get() + ).start(next: { [weak self] playbackRate, videoQuality in + guard let self else { return } - - let effectiveBaseRate: Double - if isShowingContextMenu { - effectiveBaseRate = 1.0 - } else { - effectiveBaseRate = playbackRate + + var rateString: String? + if abs(playbackRate - 1.0) > 0.05 { + var stringValue = String(format: "%.1fx", playbackRate) + if stringValue.hasSuffix(".0x") { + stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x") + } + rateString = stringValue + } + + var qualityString: String? + if case let .quality(quality) = videoQuality { + if quality <= 360 { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityLow + } else if quality <= 480 { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityMedium + } else if quality <= 720 { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityHD + } else if quality <= 1080 { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityFHD + } else { + qualityString = self.presentationData.strings.Gallery_VideoSettings_IconQualityQHD + } } - if abs(effectiveBaseRate - strongSelf.moreBarButtonRate) > 0.01 { - strongSelf.moreBarButtonRate = effectiveBaseRate - let animated: Bool - if let moreBarButtonRateTimestamp = strongSelf.moreBarButtonRateTimestamp { - animated = CFAbsoluteTimeGetCurrent() > (moreBarButtonRateTimestamp + 0.2) - } else { - animated = false - } - strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent() - - if abs(effectiveBaseRate - 1.0) > 0.01 { - var stringValue = String(format: "%.1fx", effectiveBaseRate) - if stringValue.hasSuffix(".0x") { - stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x") - } - strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: stringValue, isLarge: true)), animated: animated) - } else { - strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated) - } - } else { - if strongSelf.moreBarButtonRateTimestamp == nil { - strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent() - } + self.settingsBarButton.setBadges(speed: rateString, quality: qualityString, transition: .spring(duration: 0.35)) + })) + + self.settingsButtonStateDisposable.set((self.isShowingSettingsMenuPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] isShowingSettingsMenu in + guard let self else { + return } - }))*/ + self.settingsBarButton.setIsMenuOpen(isMenuOpen: isShowingSettingsMenu) + })) self.statusDisposable.set((combineLatest(queue: .mainQueue(), videoNode.status, mediaFileStatus) |> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in @@ -1880,6 +2166,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } self.playbackRatePromise.set(self.playbackRate ?? 1.0) + self.videoQualityPromise.set(self.videoQuality) var isAd = false if let contentInfo = item.contentInfo { @@ -2361,7 +2648,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if videoNode.hasAttachedContext { if self.isPaused || !self.keepSoundOnDismiss { - videoNode.continuePlayingWithoutSound() + if let item = self.item, item.content is HLSVideoContent { + } else { + videoNode.continuePlayingWithoutSound() + } } } } else { @@ -2468,6 +2758,20 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) } + override func maybePerformActionForSwipeDismiss() -> Bool { + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_swipe_pip"] { + return false + } + + if #available(iOS 15.0, *) { + if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl { + nativePictureInPictureContent.beginPictureInPicture() + return true + } + } + return false + } + override func title() -> Signal { return self._title.get() } @@ -2596,14 +2900,32 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } - if !(item.content is NativeVideoContent) { + if videoNode.getVideoLayer() == nil { return } var useNative = true - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_native_pip"] { + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_native_pip_v2"] { useNative = false } + var isAd = false + if let contentInfo = item.contentInfo { + switch contentInfo { + case let .message(message, _): + isAd = message.adAttribute != nil + self.footerContentNode.setMessage(message, displayInfo: !item.displayInfoOnTop, peerIsCopyProtected: item.peerIsCopyProtected) + case let .webPage(webPage, media, _): + self.footerContentNode.setWebPage(webPage, media: media) + } + } + if isAd { + useNative = false + } + if let content = item.content as? NativeVideoContent { + if content.fileReference.media.isAnimated { + useNative = false + } + } if !useNative { return } @@ -2623,6 +2945,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if #available(iOS 15.0, *) { + var didExpand = false let content = NativePictureInPictureContentImpl(context: self.context, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: videoNode, canSkip: true, willBegin: { [weak self] content in guard let self, let controller = self.galleryController(), let navigationController = self.baseNavigationController() else { return @@ -2634,12 +2957,30 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in self?.completeCustomDismiss(true) }) + if let videoNode = self.videoNode { + videoNode.setNativePictureInPictureIsActive(false) + } + didExpand = false }, didBegin: { [weak self] _ in guard let self else { return } let _ = self + }, didEnd: { [weak self] _ in + guard let self else { + return + } + if let videoNode = self.videoNode { + videoNode.setNativePictureInPictureIsActive(false) + } + + if !didExpand { + self.activePictureInPictureController = nil + self.activePictureInPictureNavigationController = nil + } }, expand: { [weak self] completion in + didExpand = true + guard let self, let activePictureInPictureController = self.activePictureInPictureController, let activePictureInPictureNavigationController = self.activePictureInPictureNavigationController else { completion() return @@ -2653,9 +2994,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }) activePictureInPictureController.view.alpha = 1.0 - activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in - completion() + activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.35, completion: { _ in }) + + completion() }) self.nativePictureInPictureContent = content @@ -2929,25 +3271,42 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { return } + var dismissImpl: (() -> Void)? - let items: Signal<[ContextMenuItem], NoError> + let items: Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError> if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { - items = self.adMenuMainItems() + items = self.adMenuMainItems() |> map { items in + return (items, []) + } } else { items = self.contextMenuMainItems(isSettings: isSettings, dismiss: { dismissImpl?() }) } - let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) - self.isShowingContextMenuPromise.set(true) + let contextController = ContextController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: sourceNode)), items: items |> map { items in + if !items.topItems.isEmpty { + return ContextController.Items(id: AnyHashable(0), content: .twoLists(items.items, items.topItems)) + } else { + return ContextController.Items(id: AnyHashable(0), content: .list(items.items)) + } + }, gesture: gesture) + if isSettings { + self.isShowingSettingsMenuPromise.set(true) + } else { + self.isShowingContextMenuPromise.set(true) + } controller.presentInGlobalOverlay(contextController) dismissImpl = { [weak contextController] in contextController?.dismiss() } contextController.dismissed = { [weak self] in - Queue.mainQueue().after(0.1, { - self?.isShowingContextMenuPromise.set(false) + Queue.mainQueue().after(isSettings ? 0.0 : 0.1, { + if isSettings { + self?.isShowingSettingsMenuPromise.set(false) + } else { + self?.isShowingContextMenuPromise.set(false) + } }) } } @@ -2977,7 +3336,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { }, iconSource: nil, action: { [weak self] _, f in f(.dismissWithoutContent) if let navigationController = self?.baseNavigationController() as? NavigationController { - navigationController.pushViewController(AdsInfoScreen(context: context, forceDark: true)) + navigationController.pushViewController(AdsInfoScreen(context: context, mode: .channel, forceDark: true)) } }))) @@ -3090,9 +3449,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } - private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { + private func contextMenuMainItems(isSettings: Bool, dismiss: @escaping () -> Void) -> Signal<(items: [ContextMenuItem], topItems: [ContextMenuItem]), NoError> { guard let videoNode = self.videoNode, let item = self.item else { - return .single([]) + return .single(([], [])) } let peer: Signal @@ -3102,72 +3461,250 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { peer = .single(nil) } - return combineLatest(queue: Queue.mainQueue(), videoNode.status, peer) - |> take(1) - |> map { [weak self] status, peer -> [ContextMenuItem] in + return combineLatest(queue: Queue.mainQueue(), + videoNode.status |> take(1), + peer, + videoNode.videoQualityStateSignal() + ) + |> map { [weak self] status, peer, videoQualityState -> (items: [ContextMenuItem], topItems: [ContextMenuItem]) in guard let status = status, let strongSelf = self else { - return [] + return ([], []) } + var topItems: [ContextMenuItem] = [] var items: [ContextMenuItem] = [] if isSettings { - var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal - var speedIconText: String = "1x" - var didSetSpeedValue = false - for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { - if abs(speed - status.baseRate) < 0.01 { - speedValue = text - speedIconText = iconText - didSetSpeedValue = true - break - } - } - if !didSetSpeedValue && status.baseRate != 1.0 { - speedValue = String(format: "%.1fx", status.baseRate) - speedIconText = speedValue - } - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in - return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor) - }, action: { c, _ in - guard let strongSelf = self else { - c?.dismiss(completion: nil) + let sliderValuePromise = ValuePromise(nil) + topItems.append(.custom(SliderContextItem(title: strongSelf.presentationData.strings.Gallery_VideoSettings_SpeedControlTitle, minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in + guard let strongSelf = self, let videoNode = strongSelf.videoNode else { return } - - c?.pushItems(items: strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }) - }))) + let newValue = normalizeValue(newValue) + videoNode.setBaseRate(newValue) + if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(newValue) + } + sliderValuePromise.set(newValue) + }), true)) - if let videoQualityState = strongSelf.videoNode?.videoQualityState(), !videoQualityState.available.isEmpty { - items.append(.separator) + if let videoQualityState, !videoQualityState.available.isEmpty { + } else { + items.append(.custom(SectionTitleContextItem(text: strongSelf.presentationData.strings.Gallery_VideoSettings_SpeedSectionTitle), false)) + for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { + let isSelected = abs(status.baseRate - rate) < 0.01 + items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get() + |> map { value in + if isSelected && value == nil { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }), action: { _, f in + f(.default) + + guard let strongSelf = self, let videoNode = strongSelf.videoNode else { + return + } + + videoNode.setBaseRate(rate) + if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) + } + }))) + } + } + + if let videoQualityState, !videoQualityState.available.isEmpty { + items.append(.custom(SectionTitleContextItem(text: strongSelf.presentationData.strings.Gallery_VideoSettings_QualitySectionTitle), false)) - //TODO:localize - - let qualityText: String - switch videoQualityState.preferred { - case .auto: + do { + let isSelected = videoQualityState.preferred == .auto + let qualityText: String = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityAuto + let textLayout: ContextMenuActionItemTextLayout if videoQualityState.current != 0 { - qualityText = "Auto (\(videoQualityState.current)p)" + textLayout = .secondLineWithValue("\(videoQualityState.current)p") } else { - qualityText = "Auto" + textLayout = .singleLine } - case let .quality(value): - qualityText = "\(value)p" + items.append(.action(ContextMenuActionItem(id: AnyHashable("q"), text: qualityText, textLayout: textLayout, icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let videoNode = self.videoNode else { + return + } + videoNode.setVideoQuality(.auto) + self.videoQualityPromise.set(.auto) + }))) } - items.append(.action(ContextMenuActionItem(text: "Video Quality", textLayout: .secondLineWithValue(qualityText), icon: { _ in - return nil - }, action: { c, _ in - guard let strongSelf = self else { - c?.dismiss(completion: nil) + for quality in videoQualityState.available { + let isSelected = videoQualityState.preferred == .quality(quality) + let qualityTitle: String + if quality <= 360 { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityLow + } else if quality <= 480 { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityMedium + } else if quality <= 720 { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityHD + } else if quality <= 1080 { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityFHD + } else { + qualityTitle = strongSelf.presentationData.strings.Gallery_VideoSettings_QualityQHD + } + items.append(.action(ContextMenuActionItem(text: qualityTitle, textLayout: .secondLineWithValue("\(quality)p"), icon: { _ in + if isSelected { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + } else { + return nil + } + }, action: { [weak self] _, f in + f(.default) + + guard let self, let videoNode = self.videoNode else { + return + } + videoNode.setVideoQuality(.quality(quality)) + self.videoQualityPromise.set(.quality(quality)) + + /*if let controller = strongSelf.galleryController() as? GalleryController { + controller.updateSharedPlaybackRate(rate) + }*/ + }))) + } + } + } else { + if let (message, maybeFile, _) = strongSelf.contentInfo(), let file = maybeFile, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_MenuSaveToGallery, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in + guard let self else { + c?.dismiss(result: .default, completion: nil) return } - c?.pushItems(items: .single(ContextController.Items(content: .list(strongSelf.contextMenuVideoQualityItems(dismiss: dismiss))))) + if let content = item.content as? HLSVideoContent { + guard let videoNode = self.videoNode, let qualityState = videoNode.videoQualityState(), !qualityState.available.isEmpty else { + return + } + if qualityState.available.isEmpty { + return + } + guard let qualitySet = HLSQualitySet(baseFile: content.fileReference) else { + return + } + + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { c, _ in + c?.popItems() + }))) + + let addItem: (Int?, FileMediaReference) -> Void = { quality, qualityFile in + guard let qualityFileSize = qualityFile.media.size else { + return + } + let fileSizeString = dataSizeString(qualityFileSize, formatting: DataSizeStringFormatting(presentationData: self.presentationData)) + let title: String + if let quality { + title = self.presentationData.strings.Gallery_SaveToGallery_Quality("\(quality)").string + } else { + title = self.presentationData.strings.Gallery_SaveToGallery_Original + } + items.append(.action(ContextMenuActionItem(text: title, textLayout: .secondLineWithValue(fileSizeString), icon: { _ in + return nil + }, action: { [weak self] c, _ in + c?.dismiss(result: .default, completion: nil) + + guard let self else { + return + } + guard let controller = self.galleryController() else { + return + } + + let saveScreen = SaveProgressScreen(context: self.context, content: .progress(self.presentationData.strings.Story_TooltipSaving, 0.0)) + controller.present(saveScreen, in: .current) + + let stringSaving = self.presentationData.strings.Story_TooltipSaving + let stringSaved = self.presentationData.strings.Story_TooltipSaved + + let saveFileReference: AnyMediaReference = qualityFile.abstract + let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference) + + let disposable = (saveSignal + |> deliverOnMainQueue).start(next: { [weak saveScreen] progress in + guard let saveScreen else { + return + } + saveScreen.content = .progress(stringSaving, progress) + }, completed: { [weak saveScreen] in + guard let saveScreen else { + return + } + saveScreen.content = .completion(stringSaved) + Queue.mainQueue().after(3.0, { [weak saveScreen] in + saveScreen?.dismiss() + }) + }) + + saveScreen.cancelled = { + disposable.dispose() + } + }))) + } + + if self.context.isPremium { + addItem(nil, content.fileReference) + } else { + #if DEBUG + addItem(nil, content.fileReference) + #endif + } + + for quality in qualityState.available { + guard let qualityFile = qualitySet.qualityFiles[quality] else { + continue + } + addItem(quality, qualityFile) + } + + c?.pushItems(items: .single(ContextController.Items(content: .list(items)))) + } else { + c?.dismiss(result: .default, completion: nil) + + switch self.fetchStatus { + case .Local: + let _ = (SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + guard let controller = self.galleryController() else { + return + } + controller.present(UndoOverlayController(presentationData: self.presentationData, content: .mediaSaved(text: self.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + }) + default: + guard let controller = self.galleryController() else { + return + } + controller.present(textAlertController(context: self.context, title: nil, text: self.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + } }))) } - } else { + + if !items.isEmpty { + items.append(.separator) + } if let (message, _, _) = strongSelf.contentInfo() { let context = strongSelf.context items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in @@ -3225,34 +3762,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - 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) - - if let strongSelf = self { - switch strongSelf.fetchStatus { - case .Local: - let _ = (SaveToCameraRoll.saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) - |> deliverOnMainQueue).start(completed: { - guard let strongSelf = self else { - return - } - guard let controller = strongSelf.galleryController() else { - return - } - controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_VideoSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - }) - default: - guard let controller = strongSelf.galleryController() else { - return - } - controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Gallery_WaitForVideoDownoad, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { - })]), in: .window(.root)) - } - } - }))) - } - if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in if let self, let navigationController = self.baseNavigationController() { @@ -3279,148 +3788,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - return items + return (items, topItems) } } - - private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> { - guard let videoNode = self.videoNode else { - return .single([]) - } - - return videoNode.status - |> take(1) - |> deliverOnMainQueue - |> map { [weak self] status -> [ContextMenuItem] in - guard let status = status, let strongSelf = self else { - return [] - } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { c, _ in - c?.popItems() - }))) - - let sliderValuePromise = ValuePromise(nil) - items.append(.custom(SliderContextItem(minValue: 0.2, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, _ in - guard let strongSelf = self, let videoNode = strongSelf.videoNode else { - return - } - let newValue = normalizeValue(newValue) - videoNode.setBaseRate(newValue) - if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(newValue) - } - sliderValuePromise.set(newValue) - }), true)) - - items.append(.separator) - - for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { - let isSelected = abs(status.baseRate - rate) < 0.01 - items.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get() - |> map { value in - if isSelected && value == nil { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }), action: { _, f in - f(.default) - - guard let strongSelf = self, let videoNode = strongSelf.videoNode else { - return - } - - videoNode.setBaseRate(rate) - if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(rate) - } - }))) - } - - return items - } - } - - private func contextMenuVideoQualityItems(dismiss: @escaping () -> Void) -> [ContextMenuItem] { - guard let videoNode = self.videoNode else { - return [] - } - guard let qualityState = videoNode.videoQualityState(), !qualityState.available.isEmpty else { - return [] - } - - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { c, _ in - c?.popItems() - }))) - - do { - let isSelected = qualityState.preferred == .auto - let qualityText: String - if qualityState.current != 0 { - qualityText = "Auto (\(qualityState.current)p)" - } else { - qualityText = "Auto" - } - items.append(.action(ContextMenuActionItem(text: qualityText, icon: { _ in - if isSelected { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }, action: { [weak self] _, f in - f(.default) - - guard let self, let videoNode = self.videoNode else { - return - } - videoNode.setVideoQuality(.auto) - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQAuto"), color: .white))) - - /*if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(rate) - }*/ - }))) - } - - for quality in qualityState.available { - //TODO:release - let isSelected = qualityState.preferred == .quality(quality) - items.append(.action(ContextMenuActionItem(text: "\(quality)p", icon: { _ in - if isSelected { - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) - } else { - return nil - } - }, action: { [weak self] _, f in - f(.default) - - guard let self, let videoNode = self.videoNode else { - return - } - videoNode.setVideoQuality(.quality(quality)) - if quality >= 700 { - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQHD"), color: .white))) - } else { - self.settingsBarButton.setContent(.image(generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/NavigationSettingsQSD"), color: .white))) - } - - /*if let controller = strongSelf.galleryController() as? GalleryController { - controller.updateSharedPlaybackRate(rate) - }*/ - }))) - } - - return items - } private var isAirPlayActive = false private var externalVideoPlayer: ExternalVideoPlayer? @@ -3530,6 +3900,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { func updateVideoQuality(_ videoQuality: UniversalVideoContentVideoQuality) { self.videoQuality = videoQuality + self.videoQualityPromise.set(videoQuality) self.videoNode?.setVideoQuality(videoQuality) } @@ -3610,6 +3981,59 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } return keyShortcuts } + + override func hasActiveEdgeAction(edge: ActiveEdge) -> Bool { + if case .right = edge { + return true + } else { + return false + } + } + + override func setActiveEdgeAction(edge: ActiveEdge?) { + guard let videoNode = self.videoNode else { + return + } + if let edge, case .right = edge { + let effectiveRate: Double + if let current = self.activeEdgeRateState { + effectiveRate = min(2.5, current.initialRate + 0.5) + self.activeEdgeRateState = (current.initialRate, effectiveRate) + } else { + guard let playbackRate = self.playbackRate else { + return + } + effectiveRate = min(2.5, playbackRate + 0.5) + self.activeEdgeRateState = (playbackRate, effectiveRate) + } + videoNode.setBaseRate(effectiveRate) + } else if let (initialRate, _) = self.activeEdgeRateState { + self.activeEdgeRateState = nil + videoNode.setBaseRate(initialRate) + } + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.35, curve: .spring)) + } + } + + override func adjustActiveEdgeAction(distance: CGFloat) { + guard let videoNode = self.videoNode else { + return + } + if let current = self.activeEdgeRateState { + var rateFraction = Double(distance) / 100.0 + rateFraction = max(0.0, min(1.0, rateFraction)) + let rateDistance = (current.initialRate + 0.5) * (1.0 - rateFraction) + 2.5 * rateFraction + let effectiveRate = max(1.0, min(2.5, rateDistance)) + self.activeEdgeRateState = (current.initialRate, effectiveRate) + videoNode.setBaseRate(effectiveRate) + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.35, curve: .spring)) + } + } + } } final class HeaderContextReferenceContentSource: ContextReferenceContentSource { diff --git a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift index 8d9774fa20..1b865e41ea 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift @@ -342,13 +342,16 @@ class GeneralChartComponentController: ChartThemeContainer { visible: chartVisibility[index]) } - if let currency, let firstValue = values.first, let color = GColor(hexString: "#dda747") { + if let currency, let firstValue = values.first, let starColor = GColor(hexString: "#dda747") { let updatedTitle: String + let color: GColor switch currency { case .ton: updatedTitle = self.strings.revenueInTon + color = firstValue.color case .xtr: updatedTitle = self.strings.revenueInStars + color = starColor } values[0] = ChartDetailsViewModel.Value( prefix: nil, diff --git a/submodules/HashtagSearchUI/BUILD b/submodules/HashtagSearchUI/BUILD index 79973de2e0..eecdc4a889 100644 --- a/submodules/HashtagSearchUI/BUILD +++ b/submodules/HashtagSearchUI/BUILD @@ -28,6 +28,10 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/Components/BlurredBackgroundComponent", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index dfdd9fbaef..811950283c 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -12,13 +12,21 @@ import AnimationCache import MultiAnimationRenderer public final class HashtagSearchController: TelegramBaseController { + public enum Mode: Equatable { + case generic + case noChat + case chatOnly + } + private let queue = Queue() private let context: AccountContext private let peer: EnginePeer? private let query: String - let all: Bool + let mode: Mode let publicPosts: Bool + let stories: Bool + let forceDark: Bool private var transitionDisposable: Disposable? private let openMessageFromSearchDisposable = MetaDisposable() @@ -33,23 +41,34 @@ public final class HashtagSearchController: TelegramBaseController { return self.displayNode as! HashtagSearchControllerNode } - public init(context: AccountContext, peer: EnginePeer?, query: String, all: Bool = false, publicPosts: Bool = false) { + public init(context: AccountContext, peer: EnginePeer?, query: String, mode: Mode = .generic, publicPosts: Bool = false, stories: Bool = false, forceDark: Bool = false) { self.context = context self.peer = peer self.query = query - self.all = all + self.mode = mode self.publicPosts = publicPosts + self.stories = stories + self.forceDark = forceDark self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = presentationData super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.title = query + var title = query + if case .chatOnly = mode, let addressName = peer?.addressName { + title = "\(query)@\(addressName)" + } + + self.title = title self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.presentationDataDisposable = (self.context.sharedContext.presentationData @@ -58,6 +77,11 @@ public final class HashtagSearchController: TelegramBaseController { let previousTheme = self.presentationData.theme let previousStrings = self.presentationData.strings + var presentationData = presentationData + if forceDark { + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index 3ef278aa27..2da8cbbe44 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -9,10 +9,13 @@ import AccountContext import ChatListUI import SegmentedControlNode import ChatListSearchItemHeader +import PeerInfoVisualMediaPaneNode +import UIKitRuntimeUtils final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { private let context: AccountContext private weak var controller: HashtagSearchController? + private let peer: EnginePeer? private var query: String private var isCashtag = false private var presentationData: PresentationData @@ -29,6 +32,9 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg private let isSearching = Promise() private var isSearchingDisposable: Disposable? + private var searchResultsCount: Int32 = 0 + private var searchResultsCountDisposable: Disposable? + private let clippingNode: ASDisplayNode private let containerNode: ASDisplayNode let currentController: ChatController? @@ -38,10 +44,13 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let globalController: ChatController? let globalChatContents: HashtagSearchGlobalChatContents? - private var globalStorySearchContext: SearchStoryListContext? - private var globalStorySearchDisposable = MetaDisposable() - private var globalStorySearchState: StoryListContext.State? - private var globalStorySearchComponentView: ComponentView? + private var storySearchContext: SearchStoryListContext? + private var storySearchDisposable = MetaDisposable() + private var storySearchState: StoryListContext.State? + private var storySearchComponentView: ComponentView? + + private var storySearchPaneNode: PeerInfoStoryPaneNode? + private var isDisplayingStories = false private var panRecognizer: InteractiveTransitionGestureRecognizer? @@ -51,19 +60,26 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg init(context: AccountContext, controller: HashtagSearchController, peer: EnginePeer?, query: String, navigationBar: NavigationBar?, navigationController: NavigationController?) { self.context = context self.controller = controller + self.peer = peer self.query = query self.navigationBar = navigationBar self.isCashtag = query.hasPrefix("$") self.presentationData = controller.presentationData + self.isDisplayingStories = controller.stories - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + var controllerParams: ChatControllerParams? + if controller.forceDark { + controllerParams = ChatControllerParams(forcedTheme: defaultDarkColorPresentationTheme, forcedWallpaper: defaultBuiltinWallpaper(data: .default, colors: defaultDarkWallpaperGradientColors.map(\.rgb), intensity: -34)) + presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.containerNode = ASDisplayNode() - self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil, cancel: { [weak controller] in + self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil, hasTabs: controller.mode != .chatOnly, cancel: { [weak controller] in controller?.dismiss() }) @@ -75,8 +91,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.recentListNode.alpha = 0.0 let navigationController = controller.navigationController as? NavigationController - if let peer, !controller.all { - self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: nil) + if let peer, controller.mode != .noChat { + self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController), params: controllerParams) self.currentController?.alwaysShowSearchResultsAsList = true self.currentController?.showListEmptyResults = true self.currentController?.customNavigationController = navigationController @@ -84,16 +100,21 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.currentController = nil } - let myChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: false) - self.myChatContents = myChatContents - self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: nil) - self.myController?.alwaysShowSearchResultsAsList = true - self.myController?.showListEmptyResults = true - self.myController?.customNavigationController = navigationController + if let _ = peer, controller.mode != .chatOnly { + let myChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: false) + self.myChatContents = myChatContents + self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default), params: controllerParams) + self.myController?.alwaysShowSearchResultsAsList = true + self.myController?.showListEmptyResults = true + self.myController?.customNavigationController = navigationController + } else { + self.myChatContents = nil + self.myController = nil + } let globalChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: true) self.globalChatContents = globalChatContents - self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: nil) + self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default), params: controllerParams) self.globalController?.alwaysShowSearchResultsAsList = true self.globalController?.showListEmptyResults = true self.globalController?.customNavigationController = navigationController @@ -117,7 +138,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.containerNode) - if controller.all { + if controller.mode == .noChat { self.isSearching.set(self.myChatContents?.searching ?? .single(false)) } else { if let _ = peer { @@ -171,9 +192,13 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } - navigationBar?.setContentNode(self.searchContentNode, animated: false) + if controller.mode != .chatOnly { + navigationBar?.setContentNode(self.searchContentNode, animated: false) + } - self.addSubnode(self.shimmerNode) + if !self.isDisplayingStories { + self.addSubnode(self.shimmerNode) + } self.searchContentNode.setQueryUpdated { [weak self] query in self?.searchQueryPromise.set(query) @@ -218,13 +243,25 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } }) + if let currentController = self.currentController { + self.searchResultsCountDisposable = (currentController.searchResultsCount.get() + |> deliverOnMainQueue).start(next: { [weak self] searchResultsCount in + guard let self else { + return + } + self.searchResultsCount = searchResultsCount + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + }) + } + self.updateStorySearch() } deinit { self.searchQueryDisposable?.dispose() self.isSearchingDisposable?.dispose() - self.globalStorySearchDisposable.dispose() + self.searchResultsCountDisposable?.dispose() + self.storySearchDisposable.dispose() } private var panAllowedDirections: InteractiveTransitionGestureRecognizerDirections { @@ -364,25 +401,30 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } private func updateStorySearch() { - self.globalStorySearchState = nil - self.globalStorySearchDisposable.set(nil) - self.globalStorySearchContext = nil + self.storySearchState = nil + self.storySearchDisposable.set(nil) + self.storySearchContext = nil if !self.query.isEmpty { - let globalStorySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(self.query)) - self.globalStorySearchDisposable.set((globalStorySearchContext.state + var peerId: EnginePeer.Id? + if self.controller?.mode == .chatOnly { + peerId = self.peer?.id + } + let storySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(peerId, self.query)) + self.storySearchDisposable.set((storySearchContext.state |> deliverOnMainQueue).startStrict(next: { [weak self] state in guard let self else { return } if state.totalCount > 0 { - self.globalStorySearchState = state + self.storySearchState = state } else { - self.globalStorySearchState = nil + self.storySearchState = nil + self.currentController?.externalSearchResultsCount = nil } - self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut)) + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) })) - self.globalStorySearchContext = globalStorySearchContext + self.storySearchContext = storySearchContext } } @@ -407,6 +449,37 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } + private func animateContentOut() { + guard let controller = self.currentController else { + return + } + controller.contentContainerNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.3, removeOnCompletion: false) + + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(30.0 as NSNumber, forKey: "inputRadius") + controller.contentContainerNode.layer.filters = [blurFilter] + controller.contentContainerNode.layer.animate(from: 0.0 as NSNumber, to: 30.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false) + } + } + + private func animateContentIn() { + guard let controller = self.currentController else { + return + } + controller.contentContainerNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + + if let blurFilter = makeBlurFilter() { + blurFilter.setValue(0.0 as NSNumber, forKey: "inputRadius") + controller.contentContainerNode.layer.filters = [blurFilter] + controller.contentContainerNode.layer.animate(from: 30.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak controller] completed in + guard let controller, completed else { + return + } + controller.contentContainerNode.layer.filters = [] + }) + } + } + func requestUpdate(transition: ContainedViewLayoutTransition) { if let (layout, navigationHeight) = self.containerLayout { let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition) @@ -427,9 +500,99 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.insertSubnode(self.clippingNode, at: 0) } + var storyParentController: ViewController? + if self.controller?.mode == .chatOnly { + storyParentController = self.currentController + } else { + storyParentController = self.globalController + } + + var currentTopInset: CGFloat = 0.0 + var globalTopInset: CGFloat = 0.0 + + var panelSearchState: StoryResultsPanelComponent.SearchState? + if let storySearchState = self.storySearchState { + if self.isDisplayingStories { + if self.searchResultsCount > 0 { + panelSearchState = .messages(self.searchResultsCount) + } + } else { + panelSearchState = .stories(storySearchState) + } + } + + if self.isDisplayingStories { + if let storySearchState = self.storySearchState { + self.currentController?.externalSearchResultsCount = Int32(storySearchState.totalCount) + } else { + self.currentController?.externalSearchResultsCount = nil + } + } else { + self.currentController?.externalSearchResultsCount = nil + } + + if let panelSearchState { + if let storyParentController { + let componentView: ComponentView + var panelTransition = ComponentTransition(transition) + if let current = self.storySearchComponentView { + componentView = current + } else { + panelTransition = .immediate + componentView = ComponentView() + self.storySearchComponentView = componentView + } + let panelSize = componentView.update( + transition: panelTransition, + component: AnyComponent(StoryResultsPanelComponent( + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + query: self.query, + peer: self.controller?.mode == .chatOnly ? self.peer : nil, + state: panelSearchState, + sideInset: layout.safeInsets.left, + action: { [weak self] in + guard let self else { + return + } + if self.controller?.mode == .chatOnly { + self.isDisplayingStories = !self.isDisplayingStories + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) + } else { + let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(nil, self.query), listContext: self.storySearchContext) + self.controller?.push(searchController) + } + } + )), + environment: {}, + containerSize: layout.size + ) + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top - 36.0), size: panelSize) + if let view = componentView.view { + if view.superview == nil { + storyParentController.view.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + panelTransition.setFrame(view: view, frame: panelFrame) + } + if self.controller?.mode == .chatOnly { + currentTopInset += panelSize.height + } else { + globalTopInset += panelSize.height + } + } + } else if let storySearchComponentView = self.storySearchComponentView { + storySearchComponentView.view?.removeFromSuperview() + self.storySearchComponentView = nil + } + if let controller = self.currentController { + var topInset: CGFloat = insets.top - 79.0 + topInset += currentTopInset + transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)) - controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 79.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) if controller.displayNode.supernode == nil { controller.viewWillAppear(false) @@ -452,52 +615,10 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg controller.beginMessageSearch(self.query) } } - + if let controller = self.globalController { var topInset: CGFloat = insets.top - 89.0 - if let state = self.globalStorySearchState { - let componentView: ComponentView - var panelTransition = ComponentTransition(transition) - if let current = self.globalStorySearchComponentView { - componentView = current - } else { - panelTransition = .immediate - componentView = ComponentView() - self.globalStorySearchComponentView = componentView - } - let panelSize = componentView.update( - transition: .immediate, - component: AnyComponent(StoryResultsPanelComponent( - context: self.context, - theme: self.presentationData.theme, - strings: self.presentationData.strings, - query: self.query, - state: state, - sideInset: layout.safeInsets.left, - action: { [weak self] in - guard let self else { - return - } - let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, scope: .query(self.query), listContext: self.globalStorySearchContext) - self.controller?.push(searchController) - } - )), - environment: {}, - containerSize: layout.size - ) - let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top - 36.0), size: panelSize) - if let view = componentView.view { - if view.superview == nil { - controller.view.addSubview(view) - view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.25, additive: true) - } - panelTransition.setFrame(view: view, frame: panelFrame) - } - topInset += panelSize.height - } else if let globalStorySearchComponentView = self.globalStorySearchComponentView { - globalStorySearchComponentView.view?.removeFromSuperview() - self.globalStorySearchComponentView = nil - } + topInset += globalTopInset transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: layout.size.width * 2.0, y: 0.0), size: layout.size)) controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) @@ -511,6 +632,75 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } + if self.isDisplayingStories, let peer = self.peer, let storySearchContext = self.storySearchContext { + let storySearchPaneNode: PeerInfoStoryPaneNode + var paneTransition = transition + if let current = self.storySearchPaneNode { + storySearchPaneNode = current + } else { + storySearchPaneNode = PeerInfoStoryPaneNode( + context: self.context, + scope: .search(peerId: peer.id, query: self.query), + captureProtected: false, + isProfileEmbedded: false, + canManageStories: false, + navigationController: { [weak self] in + guard let self else { + return nil + } + return self.controller?.navigationController as? NavigationController + }, + listContext: storySearchContext + ) + self.storySearchPaneNode = storySearchPaneNode + if let storySearchView = self.storySearchComponentView?.view { + storySearchView.superview?.insertSubview(storySearchPaneNode.view, belowSubview: storySearchView) + } else { + storyParentController?.view.addSubview(storySearchPaneNode.view) + } + paneTransition = .immediate + + if transition.isAnimated { + storySearchPaneNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + storySearchPaneNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.animateContentOut() + } + } + + var bottomInset: CGFloat = 0.0 + if case .regular = layout.metrics.widthClass { + bottomInset += 49.0 + } else { + bottomInset += 45.0 + } + bottomInset += layout.intrinsicInsets.bottom + + storySearchPaneNode.update( + size: layout.size, + topInset: navigationBarHeight, + sideInset: layout.safeInsets.left, + bottomInset: 0.0, + deviceMetrics: layout.deviceMetrics, + visibleHeight: layout.size.height - currentTopInset, + isScrollingLockedAtTop: false, + expandProgress: 1.0, + navigationHeight: 0.0, + presentationData: self.presentationData, + synchronous: false, + transition: paneTransition + ) + storySearchPaneNode.view.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + paneTransition.updateFrame(node: storySearchPaneNode, frame: CGRect(origin: CGPoint(x: 0.0, y: currentTopInset), size: CGSize(width: layout.size.width, height: layout.size.height - bottomInset - currentTopInset))) + } else if let storySearchPaneNode = self.storySearchPaneNode { + self.storySearchPaneNode = nil + + storySearchPaneNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + storySearchPaneNode.view.removeFromSuperview() + }) + storySearchPaneNode.layer.animateSublayerScale(from: 1.0, to: 0.95, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + self.animateContentIn() + } + transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: .zero, size: layout.size)) let containerPosition: CGFloat = -layout.size.width * CGFloat(self.searchContentNode.selectedIndex) - self.panTransitionFraction * layout.size.width @@ -522,7 +712,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.shimmerNode.update(context: self.context, size: CGSize(width: layout.size.width - overflowInset * 2.0, height: layout.size.height), presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, key: .chats, hasSelection: false, transition: transition) if isFirstTime { - self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode) + if self.shimmerNode.supernode != nil { + self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode) + } else { + self.insertSubnode(self.recentListNode, aboveSubnode: self.clippingNode) + } } transition.updateFrame(node: self.recentListNode, frame: CGRect(origin: .zero, size: layout.size)) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift index 33eb723ab7..d197166f39 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift @@ -16,6 +16,7 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { private var theme: PresentationTheme private let strings: PresentationStrings private let hasCurrentChat: Bool + private let hasTabs: Bool private let cancel: () -> Void @@ -60,10 +61,11 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { } } - init(theme: PresentationTheme, strings: PresentationStrings, initialQuery: String, hasCurrentChat: Bool, cancel: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, initialQuery: String, hasCurrentChat: Bool, hasTabs: Bool, cancel: @escaping () -> Void) { self.theme = theme self.strings = strings self.hasCurrentChat = hasCurrentChat + self.hasTabs = hasTabs self.cancel = cancel @@ -135,50 +137,47 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { self.searchBar.frame = searchBarFrame self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset + sideInset, rightInset: rightInset + sideInset, transition: transition) - var items: [TabSelectorComponent.Item] = [] - if self.hasCurrentChat { - items.append(TabSelectorComponent.Item(id: AnyHashable(0), title: self.strings.HashtagSearch_ThisChat)) - } - items.append(TabSelectorComponent.Item(id: AnyHashable(1), title: self.strings.HashtagSearch_MyMessages)) - items.append(TabSelectorComponent.Item(id: AnyHashable(2), title: self.strings.HashtagSearch_PublicPosts)) - - let tabSelectorSize = self.tabSelector.update( - transition: ComponentTransition(transition), - component: AnyComponent(TabSelectorComponent( - colors: TabSelectorComponent.Colors( - foreground: self.theme.list.itemSecondaryTextColor, - selection: self.theme.list.itemAccentColor - ), - customLayout: TabSelectorComponent.CustomLayout( - font: Font.medium(14.0), - spacing: self.hasCurrentChat ? 24.0 : 8.0, - lineSelection: true - ), - items: items, - selectedId: AnyHashable(self.selectedIndex), - setSelectedId: { [weak self] id in - guard let self, let index = id.base as? Int else { - return - } - self.indexUpdated?(index) - }, - transitionFraction: self.transitionFraction - )), - environment: {}, - containerSize: CGSize(width: size.width, height: 44.0) - ) - let tabSelectorFrameOriginX: CGFloat - if self.hasCurrentChat || "".isEmpty { - tabSelectorFrameOriginX = floorToScreenPixels((size.width - tabSelectorSize.width) / 2.0) - } else { - tabSelectorFrameOriginX = 4.0 - } - let tabSelectorFrame = CGRect(origin: CGPoint(x: tabSelectorFrameOriginX, y: size.height - tabSelectorSize.height - 9.0), size: tabSelectorSize) - if let tabSelectorView = self.tabSelector.view { - if tabSelectorView.superview == nil { - self.view.addSubview(tabSelectorView) + if self.hasTabs { + var items: [TabSelectorComponent.Item] = [] + if self.hasCurrentChat { + items.append(TabSelectorComponent.Item(id: AnyHashable(0), title: self.strings.HashtagSearch_ThisChat)) + } + items.append(TabSelectorComponent.Item(id: AnyHashable(1), title: self.strings.HashtagSearch_MyMessages)) + items.append(TabSelectorComponent.Item(id: AnyHashable(2), title: self.strings.HashtagSearch_PublicPosts)) + + let tabSelectorSize = self.tabSelector.update( + transition: ComponentTransition(transition), + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: self.theme.list.itemSecondaryTextColor, + selection: self.theme.list.itemAccentColor + ), + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: self.hasCurrentChat ? 24.0 : 8.0, + lineSelection: true + ), + items: items, + selectedId: AnyHashable(self.selectedIndex), + setSelectedId: { [weak self] id in + guard let self, let index = id.base as? Int else { + return + } + self.indexUpdated?(index) + }, + transitionFraction: self.transitionFraction + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 44.0) + ) + let tabSelectorFrameOriginX = floorToScreenPixels((size.width - tabSelectorSize.width) / 2.0) + let tabSelectorFrame = CGRect(origin: CGPoint(x: tabSelectorFrameOriginX, y: size.height - tabSelectorSize.height - 9.0), size: tabSelectorSize) + if let tabSelectorView = self.tabSelector.view { + if tabSelectorView.superview == nil { + self.view.addSubview(tabSelectorView) + } + transition.updateFrame(view: tabSelectorView, frame: tabSelectorFrame) } - transition.updateFrame(view: tabSelectorView, frame: tabSelectorFrame) } } diff --git a/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift index 32a693623c..6ee748f476 100644 --- a/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift +++ b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift @@ -7,13 +7,20 @@ import MultilineTextComponent import BundleIconComponent import StorySetIndicatorComponent import AccountContext +import AnimatedTextComponent +import BlurredBackgroundComponent final class StoryResultsPanelComponent: CombinedComponent { + enum SearchState: Equatable { + case stories(StoryListContext.State) + case messages(Int32) + } let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let query: String - let state: StoryListContext.State + let peer: EnginePeer? + let state: SearchState let sideInset: CGFloat let action: () -> Void @@ -22,7 +29,8 @@ final class StoryResultsPanelComponent: CombinedComponent { theme: PresentationTheme, strings: PresentationStrings, query: String, - state: StoryListContext.State, + peer: EnginePeer?, + state: SearchState, sideInset: CGFloat, action: @escaping () -> Void ) { @@ -30,6 +38,7 @@ final class StoryResultsPanelComponent: CombinedComponent { self.theme = theme self.strings = strings self.query = query + self.peer = peer self.state = state self.sideInset = sideInset self.action = action @@ -45,6 +54,9 @@ final class StoryResultsPanelComponent: CombinedComponent { if lhs.query != rhs.query { return false } + if lhs.peer != rhs.peer { + return false + } if lhs.state != rhs.state { return false } @@ -55,10 +67,11 @@ final class StoryResultsPanelComponent: CombinedComponent { } static var body: Body { - let background = Child(Rectangle.self) + let background = Child(BlurredBackgroundComponent.self) let avatars = Child(StorySetIndicatorComponent.self) + let titlePrefix = Child(AnimatedTextComponent.self) let title = Child(MultilineTextComponent.self) - let text = Child(MultilineTextComponent.self) + let text = Child(AnimatedTextComponent.self) let arrow = Child(BundleIconComponent.self) let separator = Child(Rectangle.self) let button = Child(Button.self) @@ -68,63 +81,119 @@ final class StoryResultsPanelComponent: CombinedComponent { let spacing: CGFloat = 3.0 - let textLeftInset: CGFloat = 81.0 + component.sideInset + var textLeftInset: CGFloat = 16.0 + component.sideInset let textTopInset: CGFloat = 9.0 var existingPeerIds = Set() var items: [StorySetIndicatorComponent.Item] = [] - for item in component.state.items { - guard let peer = item.peer, !existingPeerIds.contains(peer.id) else { - continue + switch component.state { + case let .stories(state): + for item in state.items { + guard let peer = item.peer, !existingPeerIds.contains(peer.id) || component.peer != nil else { + continue + } + existingPeerIds.insert(peer.id) + items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer)) } - existingPeerIds.insert(peer.id) - items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer)) + textLeftInset += 65.0 + default: + break } - - let avatars = avatars.update( - component: StorySetIndicatorComponent( - context: component.context, - strings: component.strings, - items: Array(items.prefix(3)), - displayAvatars: true, - hasUnseen: true, - hasUnseenPrivate: false, - totalCount: 0, - theme: component.theme, - action: {} - ), - availableSize: context.availableSize, - transition: .immediate - ) - - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.strings.HashtagSearch_StoriesFound(Int32(component.state.totalCount)), + + var titlePrefixString: [AnimatedTextComponent.Item] = [] + let titleString: NSAttributedString + var textString: [AnimatedTextComponent.Item] = [] + if let peer = component.peer, let username = peer.addressName { + let entityType: String + switch component.state { + case let .messages(count): + titlePrefixString = [AnimatedTextComponent.Item( + id: "text", + isUnbreakable: true, + content: .text(component.strings.HashtagSearch_Posts(count)) + )] + entityType = component.strings.HashtagSearch_FoundPosts + case let .stories(state): + titlePrefixString = [AnimatedTextComponent.Item( + id: "text", + isUnbreakable: true, + content: .text(component.strings.HashtagSearch_Stories(Int32(state.totalCount))) + )] + entityType = component.strings.HashtagSearch_FoundStories + } + let fullString = component.strings.HashtagSearch_LocalStoriesFound("", "@\(username)") + titleString = NSMutableAttributedString( + string: fullString.string, + font: Font.semibold(15.0), + textColor: component.theme.rootController.navigationBar.primaryTextColor, + paragraphAlignment: .natural + ) + if let lastRange = fullString.ranges.last?.range { + (titleString as? NSMutableAttributedString)?.addAttribute(NSAttributedString.Key.foregroundColor, value: component.theme.rootController.navigationBar.accentTextColor, range: lastRange) + } + textString = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.HashtagSearch_FoundInfoFormat( + ".", + "." + ), id: "info", mapping: [ + 0: .text(entityType), + 1: .text(component.query) + ]) + } else { + if case let .stories(state) = component.state { + titleString = NSAttributedString( + string: component.strings.HashtagSearch_StoriesFound(Int32(state.totalCount)), font: Font.semibold(15.0), textColor: component.theme.rootController.navigationBar.primaryTextColor, paragraphAlignment: .natural - )), + ) + } else { + titleString = NSAttributedString() + } + textString = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.HashtagSearch_FoundInfoFormat( + ".", + "." + ), id: "info", mapping: [ + 0: .text(component.strings.HashtagSearch_FoundStories), + 1: .text(component.query) + ]) + } + + var titlePrefixOffset: CGFloat = 0.0 + var titlePrefixChild: _UpdatedChildComponent? + if !titlePrefixString.isEmpty { + let titlePrefix = titlePrefix.update( + component: AnimatedTextComponent( + font: Font.semibold(15.0), + color: component.theme.rootController.navigationBar.primaryTextColor, + items: titlePrefixString, + noDelay: true + ), + availableSize: CGSize(width: context.availableSize.width - textLeftInset - 64.0, height: context.availableSize.height), + transition: context.transition + ) + titlePrefixOffset = titlePrefix.size.width + 1.0 + titlePrefixChild = titlePrefix + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(titleString), horizontalAlignment: .natural, maximumNumberOfLines: 1 ), - availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate + availableSize: CGSize(width: context.availableSize.width - textLeftInset - 64.0 - titlePrefixOffset, height: CGFloat.greatestFiniteMagnitude), + transition: context.transition ) - + let text = text.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.strings.HashtagSearch_StoriesFoundInfo(component.query).string, - font: Font.regular(14.0), - textColor: component.theme.rootController.navigationBar.secondaryTextColor, - paragraphAlignment: .natural - )), - horizontalAlignment: .natural, - maximumNumberOfLines: 1 + component: AnimatedTextComponent( + font: Font.regular(14.0), + color: component.theme.rootController.navigationBar.secondaryTextColor, + items: textString, + noDelay: true ), availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: context.availableSize.height), - transition: .immediate + transition: context.transition ) let arrow = arrow.update( @@ -139,7 +208,7 @@ final class StoryResultsPanelComponent: CombinedComponent { let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + text.size.height + textTopInset + 2.0) let background = background.update( - component: Rectangle(color: component.theme.rootController.navigationBar.opaqueBackgroundColor), + component: BlurredBackgroundComponent(color: component.theme.rootController.navigationBar.blurredBackgroundColor), availableSize: size, transition: .immediate ) @@ -167,12 +236,37 @@ final class StoryResultsPanelComponent: CombinedComponent { .position(CGPoint(x: background.size.width / 2.0, y: background.size.height - separator.size.height / 2.0)) ) - context.add(avatars - .position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0)) - ) + if !items.isEmpty { + let avatars = avatars.update( + component: StorySetIndicatorComponent( + context: component.context, + strings: component.strings, + items: Array(items.prefix(3)), + displayAvatars: component.peer == nil, + hasUnseen: true, + hasUnseenPrivate: false, + totalCount: 0, + theme: component.theme, + action: {} + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(avatars + .position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) + } + if let titlePrefixChild { + context.add(titlePrefixChild + .position(CGPoint(x: textLeftInset + titlePrefixChild.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + } + context.add(title - .position(CGPoint(x: textLeftInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + .position(CGPoint(x: textLeftInset + titlePrefixOffset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) ) context.add(text diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index b5a98b2eef..121fcbcb6b 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -1345,7 +1345,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } default: strongSelf.loadProgress.set(1.0) - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in switch navigation { case let .chat(_, subject, peekData): if let navigationController = strongSelf.getNavigationController() { diff --git a/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift b/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift index c629bd0b4e..cb68336005 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift @@ -86,8 +86,10 @@ final class ItemListControllerTabsContentNode: NavigationBarContentNode { ), customLayout: TabSelectorComponent.CustomLayout( font: Font.medium(14.0), - spacing: 24.0, - lineSelection: true + spacing: 48.0, + innerSpacing: 0.0, + lineSelection: true, + allowScroll: false ), items: mappedItems, selectedId: AnyHashable(self.index), diff --git a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift index 7eae8757e6..b9ccc205fa 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListTextItem.swift @@ -14,29 +14,63 @@ public enum ItemListTextItemText { case large(String) case markdown(String) case custom(context: AccountContext, string: NSAttributedString) + + var text: String { + switch self { + case let .plain(text), let .large(text), let .markdown(text): + return text + case let .custom(_, string): + return string.string + } + } } public enum ItemListTextItemLinkAction { case tap(String) } +public enum ItemListTextItemTextAlignment { + case natural + case center + + var textAlignment: NSTextAlignment { + switch self { + case .natural: + return .natural + case .center: + return .center + } + } +} + +public enum ItemListTextItemTextSize { + case generic + case larger +} + public class ItemListTextItem: ListViewItem, ItemListItem { let presentationData: ItemListPresentationData let text: ItemListTextItemText public let sectionId: ItemListSectionId let linkAction: ((ItemListTextItemLinkAction) -> Void)? let style: ItemListStyle + let textSize: ItemListTextItemTextSize + let textAlignment: ItemListTextItemTextAlignment let trimBottomInset: Bool + let additionalInsets: UIEdgeInsets public let isAlwaysPlain: Bool = true public let tag: ItemListItemTag? - public init(presentationData: ItemListPresentationData, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks, tag: ItemListItemTag? = nil, trimBottomInset: Bool = false) { + public init(presentationData: ItemListPresentationData, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil, style: ItemListStyle = .blocks, textSize: ItemListTextItemTextSize = .generic, textAlignment: ItemListTextItemTextAlignment = .natural, tag: ItemListItemTag? = nil, trimBottomInset: Bool = false, additionalInsets: UIEdgeInsets = .zero) { self.presentationData = presentationData self.text = text self.sectionId = sectionId self.linkAction = linkAction self.style = style + self.textSize = textSize + self.textAlignment = textAlignment self.trimBottomInset = trimBottomInset + self.additionalInsets = additionalInsets self.tag = tag } @@ -127,13 +161,19 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { return { [weak self] item, params, neighbors in let leftInset: CGFloat = 15.0 - let topInset: CGFloat = 7.0 + var topInset: CGFloat = 7.0 var bottomInset: CGFloat = 7.0 - let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) - let largeTitleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize)) + var titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize) + var textColor: UIColor = item.presentationData.theme.list.freeTextColor + if case .large = item.text { + titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize)) + } else if case .larger = item.textSize { + titleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize / 17.0 * 15.0)) + textColor = item.presentationData.theme.list.itemSecondaryTextColor + } let titleBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseHeaderFontSize) - + var themeUpdated = false var chevronImage = currentChevronImage if currentItem?.presentationData.theme !== item.presentationData.theme { @@ -145,14 +185,19 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { case let .plain(text): attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.freeTextColor) case let .large(text): - attributedText = NSAttributedString(string: text, font: largeTitleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) case let .markdown(text): - let mutableAttributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.presentationData.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in + let mutableAttributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: textColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: textColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) - })).mutableCopy() as! NSMutableAttributedString + }), textAlignment: item.textAlignment.textAlignment).mutableCopy() as! NSMutableAttributedString if let _ = text.range(of: ">]"), let range = mutableAttributedText.string.range(of: ">") { if themeUpdated || currentChevronImage == nil { - chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor) + switch item.textSize { + case .generic: + chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: item.presentationData.theme.list.itemAccentColor) + case .larger: + chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: item.presentationData.theme.list.itemAccentColor) + } } if let chevronImage { mutableAttributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: mutableAttributedText.string)) @@ -162,7 +207,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { case let .custom(_, string): attributedText = string } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset * 2.0 - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: item.textAlignment.textAlignment, cutout: nil, insets: UIEdgeInsets())) let contentSize: CGSize @@ -174,6 +219,10 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { default: break } + + topInset += item.additionalInsets.top + bottomInset += item.additionalInsets.bottom + contentSize = CGSize(width: params.width, height: titleLayout.size.height + topInset + bottomInset) if item.trimBottomInset { @@ -202,7 +251,15 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } let _ = titleApply(textArguments) - strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset + params.leftInset, y: topInset), size: titleLayout.size) + let titleOrigin: CGFloat + switch item.textAlignment { + case .natural: + titleOrigin = leftInset + params.leftInset + case .center: + titleOrigin = floorToScreenPixels((contentSize.width - titleLayout.size.width) / 2.0) + } + + strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: topInset), size: titleLayout.size) } }) } @@ -261,7 +318,7 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { } } - if let rects = rects { + if var rects { let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { linkHighlightingNode = current @@ -270,6 +327,10 @@ public class ItemListTextItemNode: ListViewItemNode, ItemListItemNode { self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } + if item.text.text.contains(">]"), var lastRect = rects.last { + lastRect.size.width += 8.0 + rects[rects.count - 1] = lastRect + } linkHighlightingNode.frame = self.textNode.textNode.frame linkHighlightingNode.updateRects(rects) } else if let linkHighlightingNode = self.linkHighlightingNode { diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index fde42317a1..801468bc2c 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -85,7 +85,7 @@ public final class JoinLinkPreviewController: ViewController { strongSelf.controllerNode.setRequestPeer(image: invite.photoRepresentation, title: invite.title, about: invite.about, memberCount: invite.participantsCount, isGroup: !invite.flags.isBroadcast, isVerified: invite.flags.isVerified, isFake: invite.flags.isFake, isScam: invite.flags.isScam) } else { let data = JoinLinkPreviewData(isGroup: !invite.flags.isBroadcast, isJoined: false) - strongSelf.controllerNode.setInvitePeer(image: invite.photoRepresentation, title: invite.title, memberCount: invite.participantsCount, members: invite.participants?.map({ $0 }) ?? [], data: data) + strongSelf.controllerNode.setInvitePeer(image: invite.photoRepresentation, title: invite.title, about: invite.about, memberCount: invite.participantsCount, members: invite.participants?.map({ $0 }) ?? [], data: data) } case let .alreadyJoined(peer): strongSelf.navigateToPeer(peer, nil) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift index 072a48a65d..7516769aef 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift @@ -392,8 +392,8 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, ASScrollVi self.setNeedsLayout() } - func setInvitePeer(image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) { - let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .invite(isGroup: data.isGroup, image: image, title: title, memberCount: memberCount, members: members)) + func setInvitePeer(image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, members: [EnginePeer], data: JoinLinkPreviewData) { + let contentNode = JoinLinkPreviewPeerContentNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, content: .invite(isGroup: data.isGroup, image: image, title: title, about: about, memberCount: memberCount, members: members)) contentNode.join = { [weak self] in self?.join?() } diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index 60b8ea8e4c..6acb4bc232 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -32,33 +32,33 @@ private final class MoreNode: ASDisplayNode { final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainerNode { enum Content { - case invite(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, memberCount: Int32, members: [EnginePeer]) + case invite(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, members: [EnginePeer]) case request(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, isVerified: Bool, isFake: Bool, isScam: Bool) var isGroup: Bool { switch self { - case let .invite(isGroup, _, _, _, _), let .request(isGroup, _, _, _, _, _, _, _): + case let .invite(isGroup, _, _, _, _, _), let .request(isGroup, _, _, _, _, _, _, _): return isGroup } } var image: TelegramMediaImageRepresentation? { switch self { - case let .invite(_, image, _, _, _), let .request(_, image, _, _, _, _, _, _): + case let .invite(_, image, _, _, _, _), let .request(_, image, _, _, _, _, _, _): return image } } var title: String { switch self { - case let .invite(_, _, title, _, _), let .request(_, _, title, _, _, _, _, _): + case let .invite(_, _, title, _, _, _), let .request(_, _, title, _, _, _, _, _): return title } } var memberCount: Int32 { switch self { - case let .invite(_, _, _, memberCount, _), let .request(_, _, _, _, memberCount, _, _, _): + case let .invite(_, _, _, _, memberCount, _), let .request(_, _, _, _, memberCount, _, _, _): return memberCount } } @@ -138,7 +138,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor) - if case let .invite(isGroup, _, _, memberCount, members) = content { + if case let .invite(isGroup, _, _, _, memberCount, members) = content { self.peerNodes = members.compactMap { peer in guard peer.id != context.account.peerId else { return nil @@ -176,7 +176,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer self.addSubnode(self.countNode) let membersString: String if content.isGroup { - if case let .invite(_, _, _, memberCount, members) = content, !members.isEmpty { + if case let .invite(_, _, _, _, memberCount, members) = content, !members.isEmpty { membersString = strings.Invitation_Members(memberCount) } else { membersString = strings.Conversation_StatusMembers(content.memberCount) @@ -195,7 +195,13 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer } self.moreNode.flatMap(self.peersScrollNode.addSubnode) - if case let .request(isGroup, _, _, about, _, _, _, _) = content { + switch content { + case let .invite(_, _, _, about, _, _): + if let about = about, !about.isEmpty { + self.aboutNode.attributedText = NSAttributedString(string: about, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + self.addSubnode(self.aboutNode) + } + case let .request(isGroup, _, _, about, _, _, _, _): if let about = about, !about.isEmpty { self.aboutNode.attributedText = NSAttributedString(string: about, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) self.addSubnode(self.aboutNode) @@ -339,12 +345,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0), size: countSize)) var verticalOffset = verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0 + countSize.height + 18.0 - - if let aboutSize = aboutSize { - transition.updateFrame(node: self.aboutNode, frame: CGRect(origin: CGPoint(x: floor((size.width - aboutSize.width) / 2.0), y: verticalOffset), size: aboutSize)) - verticalOffset += aboutSize.height + 20.0 - } - + let peerSize = CGSize(width: 85.0, height: 95.0) let peerInset: CGFloat = 10.0 @@ -367,6 +368,11 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer verticalOffset += 100.0 } + if let aboutSize = aboutSize { + transition.updateFrame(node: self.aboutNode, frame: CGRect(origin: CGPoint(x: floor((size.width - aboutSize.width) / 2.0), y: verticalOffset), size: aboutSize)) + verticalOffset += aboutSize.height + 20.0 + } + let buttonInset: CGFloat = 16.0 let actionButtonHeight = self.actionButtonNode.updateLayout(width: size.width - buttonInset * 2.0, transition: transition) transition.updateFrame(node: self.actionButtonNode, frame: CGRect(x: buttonInset, y: verticalOffset, width: size.width, height: actionButtonHeight)) diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m index 1f41d51c92..f4c3addf66 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryModel.m @@ -403,6 +403,7 @@ TGPhotoEditorControllerIntent intent = isVideo ? TGPhotoEditorControllerVideoIntent : TGPhotoEditorControllerGenericIntent; TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:_context item:editableMediaItem intent:intent adjustments:adjustments caption:caption screenImage:screenImage availableTabs:_interfaceView.currentTabs selectedTab:tab]; + controller.modalPresentationStyle = UIModalPresentationFullScreen; controller.entitiesView = entitiesView; controller.editingContext = _editingContext; controller.stickersContext = _stickersContext; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m b/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m index ec3e9d2888..02bfd423ba 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorUtils.m @@ -26,7 +26,11 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen() if ([UIScreen mainScreen].scale >= 2.0f - FLT_EPSILON) { - if (widescreenWidth >= 932.0f - FLT_EPSILON) + if (widescreenWidth >= 956.0f - FLT_EPSILON) + { + return CGSizeMake(145.0f + TGScreenPixel, 145.0 + TGScreenPixel); + } + else if (widescreenWidth >= 932.0f - FLT_EPSILON) { return CGSizeMake(141.0f + TGScreenPixel, 141.0 + TGScreenPixel); } @@ -38,6 +42,10 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen() { return CGSizeMake(137.0f - TGScreenPixel, 137.0f - TGScreenPixel); } + else if (widescreenWidth >= 874.0f - FLT_EPSILON) + { + return CGSizeMake(133.0f - TGScreenPixel, 133.0f - TGScreenPixel); + } else if (widescreenWidth >= 852.0f - FLT_EPSILON) { return CGSizeMake(129.0f - TGScreenPixel, 129.0f - TGScreenPixel); diff --git a/submodules/LegacyComponents/Sources/TGViewController.mm b/submodules/LegacyComponents/Sources/TGViewController.mm index db80d043bc..565c3585a9 100644 --- a/submodules/LegacyComponents/Sources/TGViewController.mm +++ b/submodules/LegacyComponents/Sources/TGViewController.mm @@ -521,12 +521,10 @@ static id _defaultContext = nil; - (bool)inFormSheet { - if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { return false; - else - { - if ([self.navigationController isKindOfClass:[TGNavigationController class]]) - { + } else { + if ([self.navigationController isKindOfClass:[TGNavigationController class]]) { switch (((TGNavigationController *)self.navigationController).presentationStyle) { case TGNavigationControllerPresentationStyleInFormSheet: diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index 52cbe8cf30..ea428cb7bb 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -222,7 +222,38 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: }) } -public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTitle: String?, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal), parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: NSAttributedString, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController { +public func legacyAttachmentMenu( + context: AccountContext, + peer: Peer?, + threadTitle: String?, + chatLocation: ChatLocation, + editMediaOptions: LegacyAttachmentMenuMediaEditing?, + addingMedia: Bool, + saveEditedPhotos: Bool, + allowGrouping: Bool, + hasSchedule: Bool, + canSendPolls: Bool, + updatedPresentationData: (initial: PresentationData, signal: Signal), + parentController: LegacyController, + recentlyUsedInlineBots: [Peer], + initialCaption: NSAttributedString, + openGallery: @escaping () -> Void, + openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, + openFileGallery: @escaping () -> Void, + openWebSearch: @escaping () -> Void, + openMap: @escaping () -> Void, + openContacts: @escaping () -> Void, + openPoll: @escaping () -> Void, + presentSelectionLimitExceeded: @escaping () -> Void, + presentCantSendMultipleFiles: @escaping () -> Void, + presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, + presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, + presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, + sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, + selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, + getCaptionPanelView: @escaping () -> TGCaptionPanelView?, + present: @escaping (ViewController, Any?) -> Void +) -> TGMenuSheetController { let defaultVideoPreset = defaultVideoPresetForContext(context) UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0") @@ -382,7 +413,13 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit } itemViews.append(carouselItem) - let galleryItem = TGMenuSheetButtonItemView(title: editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in + let galleryTitle: String + if addingMedia { + galleryTitle = presentationData.strings.AttachmentMenu_AddPhotoOrVideo + } else { + galleryTitle = editing ? presentationData.strings.Conversation_EditingMessageMediaChange : presentationData.strings.AttachmentMenu_PhotoOrVideo + } + let galleryItem = TGMenuSheetButtonItemView(title: galleryTitle, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openGallery() })! @@ -395,11 +432,19 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit } } itemViews.append(galleryItem) - underlyingViews.append(galleryItem) + + if addingMedia { + let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_AddDocument, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in + controller?.dismiss(animated: true) + openFileGallery() + })! + itemViews.append(fileItem) + underlyingViews.append(fileItem) + } } - if !editing { + if !editing && !addingMedia { let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openFileGallery() @@ -408,7 +453,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit underlyingViews.append(fileItem) } - if canEditFile { + if canEditFile && !addingMedia { let fileItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openFileGallery() @@ -488,7 +533,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit itemViews.append(editCurrentItem) } - if editMediaOptions == nil { + if editMediaOptions == nil && !addingMedia { let locationItem = TGMenuSheetButtonItemView(title: presentationData.strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openMap() @@ -520,7 +565,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer?, threadTit carouselItemView?.underlyingViews = underlyingViews - if editMediaOptions == nil { + if editMediaOptions == nil && !addingMedia { for i in 0 ..< min(20, recentlyUsedInlineBots.count) { let peer = recentlyUsedInlineBots[i] let addressName = peer.addressName diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 86c841110f..b3d0492c73 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -651,7 +651,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)], alternativeRepresentations: []) + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: fileSize(path), attributes: [.FileName(fileName: name)], alternativeRepresentations: []) var attributes: [MessageAttribute] = [] let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) @@ -784,9 +784,9 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A finalDuration = duration } - if !asAnimation { + /*if !asAnimation { finalDimensions = TGFitSize(finalDimensions, CGSize(width: 848.0, height: 848.0)) - } + }*/ var previewRepresentations: [TelegramMediaImageRepresentation] = [] if let thumbnail = thumbnail { diff --git a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift index 59440be2d5..7b4b0095c4 100644 --- a/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift +++ b/submodules/ListSectionHeaderNode/Sources/ListSectionHeaderNode.swift @@ -67,14 +67,7 @@ public final class ListSectionHeaderNode: ASDisplayNode { } } if let action = self.action { - let actionColor: UIColor - switch self.actionType { - case .generic: - actionColor = self.theme.chatList.sectionHeaderTextColor - case .destructive: - actionColor = self.theme.list.itemDestructiveColor - } - self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: actionColor) + self.updateActionTitle() self.actionButton?.accessibilityLabel = action self.actionButton?.accessibilityTraits = [.button] } @@ -115,16 +108,34 @@ public final class ListSectionHeaderNode: ASDisplayNode { return super.hitTest(point, with: event) } + private func updateActionTitle() { + guard let action = self.action else { + return + } + let actionColor: UIColor + switch self.actionType { + case .generic: + actionColor = self.theme.chatList.sectionHeaderTextColor + case .destructive: + actionColor = self.theme.list.itemDestructiveColor + } + let attributedText = NSMutableAttributedString(string: action, font: actionFont, textColor: actionColor) + if let range = attributedText.string.range(of: ">"), let arrowImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") { + attributedText.addAttribute(.attachment, value: arrowImage, range: NSRange(range, in: attributedText.string)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedText.string)) + } + self.actionButtonLabel?.attributedText = attributedText + } + public func updateTheme(theme: PresentationTheme) { if self.theme !== theme { self.theme = theme + + self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor self.label.attributedText = NSAttributedString(string: self.title ?? "", font: titleFont, textColor: self.theme.chatList.sectionHeaderTextColor) - - self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor - if let action = self.action { - self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor) - } + + self.updateActionTitle() if let (size, leftInset, rightInset) = self.validLayout { self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset) diff --git a/submodules/MediaPasteboardUI/BUILD b/submodules/MediaPasteboardUI/BUILD index df60617029..ced1985f41 100644 --- a/submodules/MediaPasteboardUI/BUILD +++ b/submodules/MediaPasteboardUI/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/AttachmentUI:AttachmentUI", "//submodules/MediaPickerUI:MediaPickerUI", + "//submodules/AttachmentTextInputPanelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift index e4aea03f6f..eaf4f3d86f 100644 --- a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift +++ b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift @@ -8,6 +8,7 @@ import AttachmentUI import MediaPickerUI import AccountContext import LegacyComponents +import AttachmentTextInputPanelNode public func mediaPasteboardScreen( context: AccountContext, @@ -15,9 +16,10 @@ public func mediaPasteboardScreen( peer: EnginePeer, subjects: [MediaPickerScreen.Subject.Media], presentMediaPicker: @escaping (_ subject: MediaPickerScreen.Subject, _ saveEditedPhotos: Bool, _ bannedSendPhotos: (Int32, Bool)?, _ bannedSendVideos: (Int32, Bool)?, _ present: @escaping (MediaPickerScreen, AttachmentMediaPickerContext?) -> Void) -> Void, - getSourceRect: (() -> CGRect?)? = nil + getSourceRect: (() -> CGRect?)? = nil, + makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil } ) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView) controller.requestController = { _, present in presentMediaPicker(.media(subjects), false, nil, nil, { mediaPicker, mediaPickerContext in present(mediaPicker, mediaPickerContext) diff --git a/submodules/MediaPlayer/Package.swift b/submodules/MediaPlayer/Package.swift index a35bec3fb0..9c829fadc0 100644 --- a/submodules/MediaPlayer/Package.swift +++ b/submodules/MediaPlayer/Package.swift @@ -42,6 +42,8 @@ let package = Package( "MediaPlayerNode.swift", "MediaPlayerAudioRenderer.swift", "MediaPlayerFramePreview.swift", - "VideoPlayerProxy.swift"]), + "VideoPlayerProxy.swift", + "ChunkMediaPlayer.swift" + ]), ] ) diff --git a/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift b/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift new file mode 100644 index 0000000000..007c6eb247 --- /dev/null +++ b/submodules/MediaPlayer/Sources/ChunkMediaPlayer.swift @@ -0,0 +1,1202 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import CoreMedia +import TelegramCore +import TelegramAudio + +private struct ChunkMediaPlayerControlTimebase { + let timebase: CMTimebase + let isAudio: Bool +} + +private enum ChunkMediaPlayerPlaybackAction { + case play + case pause +} + +private final class ChunkMediaPlayerPartLoadedState { + let part: ChunkMediaPlayerPart + let frameSource: MediaFrameSource + var mediaBuffersDisposable: Disposable? + var mediaBuffers: MediaPlaybackBuffers? + var extraVideoFrames: ([MediaTrackFrame], CMTime)? + + init(part: ChunkMediaPlayerPart, frameSource: MediaFrameSource, mediaBuffers: MediaPlaybackBuffers?) { + self.part = part + self.frameSource = frameSource + self.mediaBuffers = mediaBuffers + } + + deinit { + self.mediaBuffersDisposable?.dispose() + } +} + +private final class ChunkMediaPlayerLoadedState { + var partStates: [ChunkMediaPlayerPartLoadedState] = [] + var controlTimebase: ChunkMediaPlayerControlTimebase? + var lostAudioSession: Bool = false +} + +private struct ChunkMediaPlayerSeekState { + let duration: Double +} + +private enum ChunkMediaPlayerState { + case paused + case playing +} + +public enum ChunkMediaPlayerActionAtEnd { + case loop((() -> Void)?) + case action(() -> Void) + case loopDisablingSound(() -> Void) + case stop +} + +public enum ChunkMediaPlayerPlayOnceWithSoundActionAtEnd { + case loop + case loopDisablingSound + case stop + case repeatIfNeeded +} + +public enum ChunkMediaPlayerStreaming { + case none + case conservative + case earlierStart + case story + + public var enabled: Bool { + if case .none = self { + return false + } else { + return true + } + } + + public var parameters: (Double, Double, Double) { + switch self { + case .none, .conservative: + return (1.0, 2.0, 3.0) + case .earlierStart: + return (1.0, 1.0, 2.0) + case .story: + return (0.25, 0.5, 1.0) + } + } + + public var isSeekable: Bool { + switch self { + case .none, .conservative, .earlierStart: + return true + case .story: + return false + } + } +} + +private final class MediaPlayerAudioRendererContext { + let renderer: MediaPlayerAudioRenderer + var requestedFrames = false + + init(renderer: MediaPlayerAudioRenderer) { + self.renderer = renderer + } +} + +public final class ChunkMediaPlayerPart { + public struct Id: Hashable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + public let startTime: Double + public let endTime: Double + public let file: TempBoxFile + + public var id: Id { + return Id(rawValue: self.file.path) + } + + public init(startTime: Double, endTime: Double, file: TempBoxFile) { + self.startTime = startTime + self.endTime = endTime + self.file = file + } +} + +public final class ChunkMediaPlayerPartsState { + public let duration: Double? + public let parts: [ChunkMediaPlayerPart] + + public init(duration: Double?, parts: [ChunkMediaPlayerPart]) { + self.duration = duration + self.parts = parts + } +} + +private final class ChunkMediaPlayerContext { + private let queue: Queue + private let postbox: Postbox + private let audioSessionManager: ManagedAudioSession + + private var partsState = ChunkMediaPlayerPartsState(duration: nil, parts: []) + + private let video: Bool + private var enableSound: Bool + private var baseRate: Double + private var playAndRecord: Bool + private var soundMuted: Bool + private var ambient: Bool + private var mixWithOthers: Bool + private var keepAudioSessionWhilePaused: Bool + private var continuePlayingWithoutSoundOnLostAudioSession: Bool + private let isAudioVideoMessage: Bool + private let onSeeked: () -> Void + + private var seekId: Int = 0 + private var initialSeekTimestamp: Double? + private var notifySeeked: Bool = false + + private let loadedState: ChunkMediaPlayerLoadedState + private var isSeeking: Bool = false + private var state: ChunkMediaPlayerState = .paused + private var audioRenderer: MediaPlayerAudioRendererContext? + private var forceAudioToSpeaker = false + fileprivate let videoRenderer: VideoPlayerProxy + + private var tickTimer: SwiftSignalKit.Timer? + + private var lastStatusUpdateTimestamp: Double? + private let playerStatus: Promise + private let playerStatusValue = Atomic(value: nil) + private let audioLevelPipe: ValuePipe + + fileprivate var actionAtEnd: ChunkMediaPlayerActionAtEnd = .stop + + private var stoppedAtEnd = false + + private var partsDisposable: Disposable? + + init( + queue: Queue, + postbox: Postbox, + audioSessionManager: ManagedAudioSession, + playerStatus: Promise, + audioLevelPipe: ValuePipe, + partsState: Signal, + video: Bool, + playAutomatically: Bool, + enableSound: Bool, + baseRate: Double, + playAndRecord: Bool, + soundMuted: Bool, + ambient: Bool, + mixWithOthers: Bool, + keepAudioSessionWhilePaused: Bool, + continuePlayingWithoutSoundOnLostAudioSession: Bool, + isAudioVideoMessage: Bool, + onSeeked: @escaping () -> Void + ) { + assert(queue.isCurrent()) + + self.queue = queue + self.postbox = postbox + self.audioSessionManager = audioSessionManager + self.playerStatus = playerStatus + self.audioLevelPipe = audioLevelPipe + self.video = video + self.enableSound = enableSound + self.baseRate = baseRate + self.playAndRecord = playAndRecord + self.soundMuted = soundMuted + self.ambient = ambient + self.mixWithOthers = mixWithOthers + self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused + self.continuePlayingWithoutSoundOnLostAudioSession = continuePlayingWithoutSoundOnLostAudioSession + self.isAudioVideoMessage = isAudioVideoMessage + self.onSeeked = onSeeked + + self.videoRenderer = VideoPlayerProxy(queue: queue) + + self.loadedState = ChunkMediaPlayerLoadedState() + + let queue = self.queue + let audioRendererContext = MediaPlayerAudioRenderer( + audioSession: .manager(self.audioSessionManager), + forAudioVideoMessage: self.isAudioVideoMessage, + playAndRecord: self.playAndRecord, + soundMuted: self.soundMuted, + ambient: self.ambient, + mixWithOthers: self.mixWithOthers, + forceAudioToSpeaker: self.forceAudioToSpeaker, + baseRate: self.baseRate, + audioLevelPipe: self.audioLevelPipe, + updatedRate: { [weak self] in + queue.async { + guard let self else { + return + } + self.tick() + } + }, + audioPaused: { [weak self] in + queue.async { + guard let self else { + return + } + if self.enableSound { + if self.continuePlayingWithoutSoundOnLostAudioSession { + self.continuePlayingWithoutSound(seek: .start) + } else { + self.pause(lostAudioSession: true, faded: false) + } + } else { + self.seek(timestamp: 0.0, action: .play, notify: true) + } + } + } + ) + self.audioRenderer = MediaPlayerAudioRendererContext(renderer: audioRendererContext) + + self.loadedState.controlTimebase = ChunkMediaPlayerControlTimebase(timebase: audioRendererContext.audioTimebase, isAudio: true) + + self.videoRenderer.visibilityUpdated = { [weak self] value in + assert(queue.isCurrent()) + + if let strongSelf = self, !strongSelf.enableSound || strongSelf.continuePlayingWithoutSoundOnLostAudioSession { + switch strongSelf.state { + case .paused: + if value { + strongSelf.play() + } + case .playing: + if !value { + strongSelf.pause(lostAudioSession: false) + } + } + } + } + + self.videoRenderer.takeFrameAndQueue = (queue, { [weak self] in + assert(queue.isCurrent()) + + guard let self else { + return .noFrames + } + + var ignoreEmptyExtraFrames = false + for i in 0 ..< self.loadedState.partStates.count { + let partState = self.loadedState.partStates[i] + + if let (extraVideoFrames, atTime) = partState.extraVideoFrames { + partState.extraVideoFrames = nil + + if extraVideoFrames.isEmpty { + if !ignoreEmptyExtraFrames { + return .restoreState(frames: extraVideoFrames, atTimestamp: atTime, soft: i != 0) + } + } else { + return .restoreState(frames: extraVideoFrames, atTimestamp: atTime, soft: i != 0) + } + } + + if let videoBuffer = partState.mediaBuffers?.videoBuffer { + let frame = videoBuffer.takeFrame() + switch frame { + case .finished: + ignoreEmptyExtraFrames = true + continue + default: + if ignoreEmptyExtraFrames, case let .frame(mediaTrackFrame) = frame { + return .restoreState(frames: [mediaTrackFrame], atTimestamp: mediaTrackFrame.position, soft: i != 0) + } + + return frame + } + } + } + + return .noFrames + }) + + audioRendererContext.start() + self.tick() + + let tickTimer = SwiftSignalKit.Timer(timeout: 1.0 / 25.0, repeat: true, completion: { [weak self] in + self?.tick() + }, queue: self.queue) + self.tickTimer = tickTimer + tickTimer.start() + + self.partsDisposable = (partsState |> deliverOn(self.queue)).startStrict(next: { [weak self] partsState in + guard let self else { + return + } + self.partsState = partsState + self.tick() + }) + } + + deinit { + assert(self.queue.isCurrent()) + + self.tickTimer?.invalidate() + self.partsDisposable?.dispose() + } + + fileprivate func seek(timestamp: Double, notify: Bool) { + assert(self.queue.isCurrent()) + + let action: ChunkMediaPlayerPlaybackAction + switch self.state { + case .paused: + action = .pause + case .playing: + action = .play + } + self.seek(timestamp: timestamp, action: action, notify: notify) + } + + fileprivate func seek(timestamp: Double, action: ChunkMediaPlayerPlaybackAction, notify: Bool) { + assert(self.queue.isCurrent()) + + self.isSeeking = true + self.loadedState.partStates.removeAll() + + self.seekId += 1 + self.initialSeekTimestamp = timestamp + self.notifySeeked = true + + switch action { + case .play: + self.state = .playing + case .pause: + self.state = .paused + } + + self.videoRenderer.flush() + + if let audioRenderer = self.audioRenderer { + let queue = self.queue + audioRenderer.renderer.flushBuffers(at: CMTime(seconds: timestamp, preferredTimescale: 44100), completion: { [weak self] in + queue.async { + guard let self else { + return + } + self.isSeeking = false + self.tick() + } + }) + } else { + if let controlTimebase = self.loadedState.controlTimebase, !controlTimebase.isAudio { + CMTimebaseSetTime(controlTimebase.timebase, time: CMTimeMakeWithSeconds(timestamp, preferredTimescale: 44000)) + } + + self.isSeeking = false + self.tick() + } + } + + fileprivate func play() { + assert(self.queue.isCurrent()) + + if case .paused = self.state { + self.state = .playing + self.stoppedAtEnd = false + self.lastStatusUpdateTimestamp = nil + + if self.enableSound { + self.audioRenderer?.renderer.start() + } + + let timestamp: Double + if let controlTimebase = self.loadedState.controlTimebase { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(controlTimebase.timebase)) + } else { + timestamp = self.initialSeekTimestamp ?? 0.0 + } + + self.seek(timestamp: timestamp, action: .play, notify: false) + } + } + + fileprivate func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) { + assert(self.queue.isCurrent()) + + /*#if DEBUG + var seek = seek + if case .timecode = seek { + seek = .timecode(830.83000000000004) + } + #endif*/ + + if !self.enableSound { + self.lastStatusUpdateTimestamp = nil + self.enableSound = true + self.playAndRecord = playAndRecord + + var timestamp: Double + if case let .timecode(time) = seek { + timestamp = time + } else if case .none = seek, let controlTimebase = self.loadedState.controlTimebase { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(controlTimebase.timebase)) + if let duration = self.currentDuration(), duration != 0.0 { + if timestamp > duration - 2.0 { + timestamp = 0.0 + } + } + } else { + timestamp = 0.0 + } + self.seek(timestamp: timestamp, action: .play, notify: true) + } else { + if case let .timecode(time) = seek { + self.seek(timestamp: Double(time), action: .play, notify: true) + } else if case .playing = self.state { + } else { + self.play() + } + } + + self.stoppedAtEnd = false + } + + fileprivate func setSoundMuted(soundMuted: Bool) { + self.soundMuted = soundMuted + self.audioRenderer?.renderer.setSoundMuted(soundMuted: soundMuted) + } + + fileprivate func continueWithOverridingAmbientMode(isAmbient: Bool) { + if self.ambient != isAmbient { + self.ambient = isAmbient + self.audioRenderer?.renderer.reconfigureAudio(ambient: self.ambient) + } + } + + fileprivate func continuePlayingWithoutSound(seek: MediaPlayerSeek) { + if self.enableSound { + self.lastStatusUpdateTimestamp = nil + + if let controlTimebase = self.loadedState.controlTimebase { + self.enableSound = false + self.playAndRecord = false + + var timestamp: Double + if case let .timecode(time) = seek { + timestamp = time + } else if case .none = seek { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(controlTimebase.timebase)) + if let duration = self.currentDuration(), duration != 0.0 { + if timestamp > duration - 2.0 { + timestamp = 0.0 + } + } + } else { + timestamp = 0.0 + } + + self.seek(timestamp: timestamp, action: .play, notify: true) + } + } + } + + fileprivate func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + if self.continuePlayingWithoutSoundOnLostAudioSession != value { + self.continuePlayingWithoutSoundOnLostAudioSession = value + } + } + + fileprivate func setBaseRate(_ baseRate: Double) { + self.baseRate = baseRate + self.lastStatusUpdateTimestamp = nil + self.tick() + self.audioRenderer?.renderer.setBaseRate(baseRate) + } + + fileprivate func setForceAudioToSpeaker(_ value: Bool) { + if self.forceAudioToSpeaker != value { + self.forceAudioToSpeaker = value + + self.audioRenderer?.renderer.setForceAudioToSpeaker(value) + } + } + + fileprivate func setKeepAudioSessionWhilePaused(_ value: Bool) { + if self.keepAudioSessionWhilePaused != value { + self.keepAudioSessionWhilePaused = value + + var isPlaying = false + switch self.state { + case .playing: + isPlaying = true + default: + break + } + if value && !isPlaying { + self.audioRenderer?.renderer.stop() + } else { + self.audioRenderer?.renderer.start() + } + } + } + + fileprivate func pause(lostAudioSession: Bool, faded: Bool = false) { + assert(self.queue.isCurrent()) + + if lostAudioSession { + self.loadedState.lostAudioSession = true + } + switch self.state { + case .paused: + break + case .playing: + self.state = .paused + self.lastStatusUpdateTimestamp = nil + + self.tick() + } + } + + fileprivate func togglePlayPause(faded: Bool) { + assert(self.queue.isCurrent()) + + switch self.state { + case .paused: + if !self.enableSound { + self.playOnceWithSound(playAndRecord: false, seek: .none) + } else { + self.play() + } + case .playing: + self.pause(lostAudioSession: false, faded: faded) + } + } + + private func currentDuration() -> Double? { + return self.partsState.duration + } + + private func tick() { + if self.isSeeking { + return + } + + var timestamp: Double + if let controlTimebase = self.loadedState.controlTimebase { + timestamp = CMTimeGetSeconds(CMTimebaseGetTime(controlTimebase.timebase)) + } else { + timestamp = self.initialSeekTimestamp ?? 0.0 + } + timestamp = max(0.0, timestamp) + + if let firstPart = self.loadedState.partStates.first, let mediaBuffers = firstPart.mediaBuffers, mediaBuffers.videoBuffer != nil, mediaBuffers.audioBuffer == nil { + // No audio + if self.audioRenderer != nil { + self.audioRenderer?.renderer.stop() + self.audioRenderer = nil + + var timebase: CMTimebase? + CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase) + let controlTimebase = ChunkMediaPlayerControlTimebase(timebase: timebase!, isAudio: false) + CMTimebaseSetTime(timebase!, time: CMTimeMakeWithSeconds(timestamp, preferredTimescale: 44000)) + + self.loadedState.controlTimebase = controlTimebase + } + } + + //print("Timestamp: \(timestamp)") + + var duration: Double = 0.0 + if let partsStateDuration = self.partsState.duration { + duration = partsStateDuration + } + + var validParts: [ChunkMediaPlayerPart] = [] + + for i in 0 ..< self.partsState.parts.count { + let part = self.partsState.parts[i] + var partMatches = false + if timestamp >= part.startTime - 0.5 && timestamp < part.endTime + 0.5 { + partMatches = true + } + + if partMatches { + validParts.append(part) + } + } + if let lastValidPart = validParts.last { + for i in 0 ..< self.partsState.parts.count { + let part = self.partsState.parts[i] + if lastValidPart !== part && part.startTime > lastValidPart.startTime && part.startTime <= lastValidPart.endTime + 0.5 { + validParts.append(part) + break + } + } + } + + /*for i in 0 ..< self.partsState.parts.count { + let part = self.partsState.parts[i] + var partMatches = false + if timestamp >= part.startTime - 0.001 && timestamp < part.endTime - 0.001 { + partMatches = true + } else if part.startTime < 0.2 && timestamp < part.endTime - 0.001 { + partMatches = true + } + + if !partMatches, i != self.partsState.parts.count - 1, part.startTime >= 0.001, timestamp >= part.startTime { + let nextPart = self.partsState.parts[i + 1] + if timestamp < nextPart.endTime - 0.001 { + if part.endTime >= nextPart.startTime - 0.1 { + partMatches = true + } + } + } + + if partMatches { + validParts.append(part) + + inner: for lookaheadPart in self.partsState.parts { + if lookaheadPart.startTime >= part.endTime - 0.001 && lookaheadPart.startTime - 0.1 < part.endTime { + validParts.append(lookaheadPart) + break inner + } + } + + break + } + }*/ + + if validParts.isEmpty, let initialSeekTimestamp = self.initialSeekTimestamp { + for part in self.partsState.parts { + if initialSeekTimestamp >= part.startTime - 0.2 && initialSeekTimestamp < part.endTime { + self.initialSeekTimestamp = nil + + self.videoRenderer.flush() + + if let audioRenderer = self.audioRenderer { + self.isSeeking = true + let queue = self.queue + audioRenderer.renderer.flushBuffers(at: CMTime(seconds: part.startTime + 0.1, preferredTimescale: 44100), completion: { [weak self] in + queue.async { + guard let self else { + return + } + self.isSeeking = false + self.tick() + } + }) + } + + return + } + } + } else { + self.initialSeekTimestamp = nil + } + + self.loadedState.partStates.removeAll(where: { partState in + if !validParts.contains(where: { $0.id == partState.part.id }) { + return true + } + return false + }) + + for part in validParts { + if !self.loadedState.partStates.contains(where: { $0.part.id == part.id }) { + let frameSource = FFMpegMediaFrameSource( + queue: self.queue, + postbox: self.postbox, + userLocation: .other, + userContentType: .other, + resourceReference: .standalone(resource: LocalFileReferenceMediaResource(localFilePath: "", randomId: 0)), + tempFilePath: part.file.path, + limitedFileRange: nil, + streamable: false, + isSeekable: true, + video: self.video, + preferSoftwareDecoding: false, + fetchAutomatically: false, + stallDuration: 1.0, + lowWaterDuration: 2.0, + highWaterDuration: 3.0, + storeAfterDownload: nil + ) + + let partState = ChunkMediaPlayerPartLoadedState( + part: part, + frameSource: frameSource, + mediaBuffers: nil + ) + self.loadedState.partStates.append(partState) + self.loadedState.partStates.sort(by: { $0.part.startTime < $1.part.startTime }) + } + } + + for i in 0 ..< self.loadedState.partStates.count { + let partState = self.loadedState.partStates[i] + if partState.mediaBuffersDisposable == nil { + partState.mediaBuffersDisposable = (partState.frameSource.seek(timestamp: i == 0 ? timestamp : 0.0) + |> deliverOn(self.queue)).startStrict(next: { [weak self, weak partState] result in + guard let self, let partState else { + return + } + guard let result = result.unsafeGet() else { + return + } + + partState.mediaBuffers = result.buffers + partState.extraVideoFrames = (result.extraDecodedVideoFrames, result.timestamp) + + if partState === self.loadedState.partStates.first { + self.audioRenderer?.renderer.flushBuffers(at: result.timestamp, completion: {}) + } + + let queue = self.queue + result.buffers.audioBuffer?.statusUpdated = { [weak self] in + queue.async { + guard let self else { + return + } + self.tick() + } + } + result.buffers.videoBuffer?.statusUpdated = { [weak self] in + queue.async { + guard let self else { + return + } + self.tick() + } + } + + self.tick() + }) + } + } + + var videoStatus: MediaTrackFrameBufferStatus? + var audioStatus: MediaTrackFrameBufferStatus? + + for i in 0 ..< self.loadedState.partStates.count { + let partState = self.loadedState.partStates[i] + + var partVideoStatus: MediaTrackFrameBufferStatus? + var partAudioStatus: MediaTrackFrameBufferStatus? + if let videoTrackFrameBuffer = partState.mediaBuffers?.videoBuffer { + partVideoStatus = videoTrackFrameBuffer.status(at: i == 0 ? timestamp : videoTrackFrameBuffer.startTime.seconds) + } + if let audioTrackFrameBuffer = partState.mediaBuffers?.audioBuffer { + partAudioStatus = audioTrackFrameBuffer.status(at: i == 0 ? timestamp : audioTrackFrameBuffer.startTime.seconds) + } + if i == 0 { + videoStatus = partVideoStatus + audioStatus = partAudioStatus + } + } + + var performActionAtEndNow = false + + var worstStatus: MediaTrackFrameBufferStatus? + for status in [videoStatus, audioStatus] { + if let status = status { + if let worst = worstStatus { + switch status { + case .buffering: + worstStatus = status + case let .full(currentFullUntil): + switch worst { + case .buffering: + worstStatus = worst + case let .full(worstFullUntil): + if currentFullUntil < worstFullUntil { + worstStatus = status + } else { + worstStatus = worst + } + case .finished: + worstStatus = status + } + case let .finished(currentFinishedAt): + switch worst { + case .buffering, .full: + worstStatus = worst + case let .finished(worstFinishedAt): + if currentFinishedAt < worstFinishedAt { + worstStatus = worst + } else { + worstStatus = status + } + } + } + } else { + worstStatus = status + } + } + } + + var rate: Double + var bufferingProgress: Float? + + if let worstStatus = worstStatus, case let .full(fullUntil) = worstStatus, fullUntil.isFinite { + var playing = false + if case .playing = self.state { + playing = true + } + if playing { + rate = self.baseRate + } else { + rate = 0.0 + } + } else if let worstStatus = worstStatus, case let .finished(finishedAt) = worstStatus, finishedAt.isFinite { + var playing = false + if case .playing = self.state { + playing = true + } + if playing { + rate = self.baseRate + } else { + rate = 0.0 + } + + //print("finished timestamp: \(timestamp), finishedAt: \(finishedAt), duration: \(duration)") + if duration > 0.0 && timestamp >= finishedAt && finishedAt >= duration - 0.2 { + performActionAtEndNow = true + } + } else if case .buffering = worstStatus { + bufferingProgress = 0.0 + rate = 0.0 + } else { + rate = 0.0 + bufferingProgress = 0.0 + } + + if rate != 0.0 && self.initialSeekTimestamp != nil { + self.initialSeekTimestamp = nil + } + + if duration > 0.0 && timestamp >= duration { + performActionAtEndNow = true + } + + var reportRate = rate + + if let controlTimebase = self.loadedState.controlTimebase { + if controlTimebase.isAudio { + if !rate.isZero { + self.audioRenderer?.renderer.start() + } + self.audioRenderer?.renderer.setRate(rate) + if !rate.isZero, let audioRenderer = self.audioRenderer { + let timebaseRate = CMTimebaseGetRate(audioRenderer.renderer.audioTimebase) + if !timebaseRate.isEqual(to: rate) { + reportRate = timebaseRate + } + } + } else { + if !CMTimebaseGetRate(controlTimebase.timebase).isEqual(to: rate) { + CMTimebaseSetRate(controlTimebase.timebase, rate: rate) + } + } + } + + if let controlTimebase = self.loadedState.controlTimebase, let videoTrackFrameBuffer = self.loadedState.partStates.first?.mediaBuffers?.videoBuffer, videoTrackFrameBuffer.hasFrames { + self.videoRenderer.state = (controlTimebase.timebase, true, videoTrackFrameBuffer.rotationAngle, videoTrackFrameBuffer.aspect) + } + + if let audioRenderer = self.audioRenderer { + let queue = self.queue + audioRenderer.requestedFrames = true + audioRenderer.renderer.beginRequestingFrames(queue: queue.queue, takeFrame: { [weak self] in + assert(queue.isCurrent()) + guard let self else { + return .noFrames + } + + for partState in self.loadedState.partStates { + if let audioTrackFrameBuffer = partState.mediaBuffers?.audioBuffer { + let frame = audioTrackFrameBuffer.takeFrame() + switch frame { + case .finished: + continue + default: + /*if case let .frame(frame) = frame { + print("audio: \(frame.position.seconds) \(frame.position.value) next: (\(frame.position.value + frame.duration.value))") + }*/ + return frame + } + } + } + + return .noFrames + }) + } + + var statusTimestamp = CACurrentMediaTime() + let playbackStatus: MediaPlayerPlaybackStatus + var isPlaying = false + var isPaused = false + if case .playing = self.state { + isPlaying = true + } else if case .paused = self.state { + isPaused = true + } + if let bufferingProgress = bufferingProgress { + playbackStatus = .buffering(initial: false, whilePlaying: isPlaying, progress: Float(bufferingProgress), display: true) + } else if !rate.isZero { + if reportRate.isZero { + playbackStatus = .playing + statusTimestamp = 0.0 + } else { + playbackStatus = .playing + } + } else { + if performActionAtEndNow && !self.stoppedAtEnd, case .loop = self.actionAtEnd, isPlaying { + playbackStatus = .playing + } else { + playbackStatus = .paused + } + } + let _ = isPaused + + if self.lastStatusUpdateTimestamp == nil || self.lastStatusUpdateTimestamp! < statusTimestamp + 1.0 / 25.0 { + self.lastStatusUpdateTimestamp = statusTimestamp + let reportTimestamp = timestamp + let statusTimestamp: Double + if duration == 0.0 { + statusTimestamp = max(reportTimestamp, 0.0) + } else { + statusTimestamp = min(max(reportTimestamp, 0.0), duration) + } + let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: statusTimestamp, baseRate: self.baseRate, seekId: self.seekId, status: playbackStatus, soundEnabled: self.enableSound) + self.playerStatus.set(.single(status)) + let _ = self.playerStatusValue.swap(status) + } + + if self.notifySeeked { + self.notifySeeked = false + self.onSeeked() + } + + if performActionAtEndNow { + if !self.stoppedAtEnd { + switch self.actionAtEnd { + case let .loop(f): + self.stoppedAtEnd = false + self.seek(timestamp: 0.0, action: .play, notify: true) + f?() + case .stop: + self.stoppedAtEnd = true + self.pause(lostAudioSession: false) + case let .action(f): + self.stoppedAtEnd = true + self.pause(lostAudioSession: false) + f() + case let .loopDisablingSound(f): + self.stoppedAtEnd = false + self.enableSound = false + self.seek(timestamp: 0.0, action: .play, notify: true) + f() + } + } + } + } +} + +public final class ChunkMediaPlayer { + private let queue = Queue() + private var contextRef: Unmanaged? + + private let statusValue = Promise() + + public var status: Signal { + return self.statusValue.get() + } + + private let audioLevelPipe = ValuePipe() + public var audioLevelEvents: Signal { + return self.audioLevelPipe.signal() + } + + public var actionAtEnd: ChunkMediaPlayerActionAtEnd = .stop { + didSet { + let value = self.actionAtEnd + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.actionAtEnd = value + } + } + } + } + + public init( + postbox: Postbox, + audioSessionManager: ManagedAudioSession, + partsState: Signal, + video: Bool, + playAutomatically: Bool = false, + enableSound: Bool, + baseRate: Double = 1.0, + playAndRecord: Bool = false, + soundMuted: Bool = false, + ambient: Bool = false, + mixWithOthers: Bool = false, + keepAudioSessionWhilePaused: Bool = false, + continuePlayingWithoutSoundOnLostAudioSession: Bool = false, + isAudioVideoMessage: Bool = false, + onSeeked: (() -> Void)? = nil + ) { + let audioLevelPipe = self.audioLevelPipe + self.queue.async { + let context = ChunkMediaPlayerContext( + queue: self.queue, + postbox: postbox, + audioSessionManager: audioSessionManager, + playerStatus: self.statusValue, + audioLevelPipe: audioLevelPipe, + partsState: partsState, + video: video, + playAutomatically: playAutomatically, + enableSound: enableSound, + baseRate: baseRate, + playAndRecord: playAndRecord, + soundMuted: soundMuted, + ambient: ambient, + mixWithOthers: mixWithOthers, + keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, + continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, + isAudioVideoMessage: isAudioVideoMessage, + onSeeked: { + onSeeked?() + } + ) + self.contextRef = Unmanaged.passRetained(context) + } + } + + deinit { + let contextRef = self.contextRef + self.queue.async { + contextRef?.release() + } + } + + public func play() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.play() + } + } + } + + public func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek = .start) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.playOnceWithSound(playAndRecord: playAndRecord, seek: seek) + } + } + } + + public func setSoundMuted(soundMuted: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setSoundMuted(soundMuted: soundMuted) + } + } + } + + public func continueWithOverridingAmbientMode(isAmbient: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.continueWithOverridingAmbientMode(isAmbient: isAmbient) + } + } + } + + public func continuePlayingWithoutSound(seek: MediaPlayerSeek = .start) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.continuePlayingWithoutSound(seek: seek) + } + } + } + + public func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setContinuePlayingWithoutSoundOnLostAudioSession(value) + } + } + } + + public func setForceAudioToSpeaker(_ value: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setForceAudioToSpeaker(value) + } + } + } + + public func setKeepAudioSessionWhilePaused(_ value: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setKeepAudioSessionWhilePaused(value) + } + } + } + + public func pause() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.pause(lostAudioSession: false) + } + } + } + + public func togglePlayPause(faded: Bool = false) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.togglePlayPause(faded: faded) + } + } + } + + public func seek(timestamp: Double, play: Bool? = nil) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + if let play { + context.seek(timestamp: timestamp, action: play ? .play : .pause, notify: false) + } else { + context.seek(timestamp: timestamp, notify: false) + } + } + } + } + + public func setBaseRate(_ baseRate: Double) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setBaseRate(baseRate) + } + } + } + + public func attachPlayerNode(_ node: MediaPlayerNode) { + let nodeRef: Unmanaged = Unmanaged.passRetained(node) + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.videoRenderer.attachNodeAndRelease(nodeRef) + } else { + Queue.mainQueue().async { + nodeRef.release() + } + } + } + } +} diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift index 8a6e6c7b70..d161b921e9 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift @@ -72,6 +72,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { private let userContentType: MediaResourceUserContentType private let resourceReference: MediaResourceReference private let tempFilePath: String? + private let limitedFileRange: Range? private let streamable: Bool private let isSeekable: Bool private let stallDuration: Double @@ -102,13 +103,14 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { } } - public init(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int? = nil, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0, storeAfterDownload: (() -> Void)? = nil) { + public init(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, limitedFileRange: Range?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int? = nil, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0, storeAfterDownload: (() -> Void)? = nil) { self.queue = queue self.postbox = postbox self.userLocation = userLocation self.userContentType = userContentType self.resourceReference = resourceReference self.tempFilePath = tempFilePath + self.limitedFileRange = limitedFileRange self.streamable = streamable self.isSeekable = isSeekable self.video = video @@ -187,6 +189,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let postbox = self.postbox let resourceReference = self.resourceReference let tempFilePath = self.tempFilePath + let limitedFileRange = self.limitedFileRange let queue = self.queue let streamable = self.streamable let isSeekable = self.isSeekable @@ -198,7 +201,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let storeAfterDownload = self.storeAfterDownload self.performWithContext { [weak self] context in - context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload) + context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload) let (frames, endOfStream) = context.takeFrames(until: timestamp, types: types) @@ -242,6 +245,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let userLocation = self.userLocation let resourceReference = self.resourceReference let tempFilePath = self.tempFilePath + let limitedFileRange = self.limitedFileRange let streamable = self.streamable let isSeekable = self.isSeekable let video = self.video @@ -259,7 +263,7 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { self.performWithContext { [weak self] context in let _ = currentSemaphore.swap(context.currentSemaphore) - context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload) + context.initializeState(postbox: postbox, userLocation: userLocation, resourceReference: resourceReference, tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamable, isSeekable: isSeekable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, fetchAutomatically: fetchAutomatically, maximumFetchSize: maximumFetchSize, storeAfterDownload: storeAfterDownload) context.seek(timestamp: timestamp, completed: { streamDescriptionsAndTimestamp in queue.async { @@ -272,12 +276,12 @@ public final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { var videoBuffer: MediaTrackFrameBuffer? if let audio = streamDescriptions.audio { - audioBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: audio.decoder, type: .audio, duration: audio.duration, rotationAngle: 0.0, aspect: 1.0, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration) + audioBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: audio.decoder, type: .audio, startTime: audio.startTime, duration: audio.duration, rotationAngle: 0.0, aspect: 1.0, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration) } var extraDecodedVideoFrames: [MediaTrackFrame] = [] if let video = streamDescriptions.video { - videoBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: video.decoder, type: .video, duration: video.duration, rotationAngle: video.rotationAngle, aspect: video.aspect, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration) + videoBuffer = MediaTrackFrameBuffer(frameSource: strongSelf, decoder: video.decoder, type: .video, startTime: video.startTime, duration: video.duration, rotationAngle: video.rotationAngle, aspect: video.aspect, stallDuration: strongSelf.stallDuration, lowWaterDuration: strongSelf.lowWaterDuration, highWaterDuration: strongSelf.highWaterDuration) for videoFrame in streamDescriptions.extraVideoFrames { if let decodedFrame = video.decoder.decode(frame: videoFrame) { extraDecodedVideoFrames.append(decodedFrame) diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift index 2cbbed178a..83b92c97cf 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift @@ -10,6 +10,7 @@ private struct StreamContext { let codecContext: FFMpegAVCodecContext? let fps: CMTime let timebase: CMTime + let startTime: CMTime let duration: CMTime let decoder: MediaTrackFrameDecoder let rotationAngle: Double @@ -17,6 +18,7 @@ private struct StreamContext { } struct FFMpegMediaFrameSourceDescription { + let startTime: CMTime let duration: CMTime let decoder: MediaTrackFrameDecoder let rotationAngle: Double @@ -63,15 +65,12 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa } var fetchedCount: Int32 = 0 - var fetchedData: Data? - /*#if DEBUG - maxOffset = max(maxOffset, context.readingOffset + Int(bufferSize)) - print("maxOffset \(maxOffset)") - #endif*/ - - let resourceSize: Int64 = resourceReference.resource.size ?? (Int64.max - 1) + var resourceSize: Int64 = resourceReference.resource.size ?? (Int64.max - 1) + if let limitedFileRange = context.limitedFileRange { + resourceSize = min(resourceSize, limitedFileRange.upperBound) + } let readCount = max(0, min(resourceSize - context.readingOffset, Int64(bufferSize))) let requestRange: Range = context.readingOffset ..< (context.readingOffset + readCount) @@ -95,9 +94,6 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa if readCount == 0 { fetchedData = Data() } else { - #if DEBUG - //print("requestRange: \(requestRange)") - #endif if let tempFilePath = context.tempFilePath, let fileData = (try? Data(contentsOf: URL(fileURLWithPath: tempFilePath), options: .mappedRead))?.subdata(in: Int(requestRange.lowerBound) ..< Int(requestRange.upperBound)) { fetchedData = fileData } else { @@ -205,7 +201,7 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe var result: Int64 = offset - let resourceSize: Int64 + var resourceSize: Int64 if let size = resourceReference.resource.size { resourceSize = size } else { @@ -238,6 +234,9 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe resourceSize = Int64.max - 1 } } + if let limitedFileRange = context.limitedFileRange { + resourceSize = min(resourceSize, limitedFileRange.upperBound) + } if (whence & FFMPEG_AVSEEK_SIZE) != 0 { result = Int64(resourceSize == Int(Int32.max - 1) ? 0 : resourceSize) @@ -252,10 +251,21 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe } else { if streamable { if context.tempFilePath == nil { - let fetchRange: Range = context.readingOffset ..< Int64.max - context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, range: (fetchRange, .elevated), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) + let fetchRange: Range? + if let limitedFileRange = context.limitedFileRange { + if context.readingOffset < limitedFileRange.upperBound { + fetchRange = context.readingOffset ..< limitedFileRange.upperBound + } else { + fetchRange = nil + } + } else { + fetchRange = context.readingOffset ..< Int64.max + } + if let fetchRange { + context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, range: (fetchRange, .elevated), statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) + } } - } else if !context.requestedCompleteFetch && context.fetchAutomatically { + } else if !context.requestedCompleteFetch && context.fetchAutomatically && context.limitedFileRange == nil { context.requestedCompleteFetch = true if context.tempFilePath == nil { context.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: resourceReference, statsCategory: statsCategory, preferBackgroundReferenceRevalidation: streamable).start()) @@ -283,6 +293,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { fileprivate var userContentType: MediaResourceUserContentType? fileprivate var resourceReference: MediaResourceReference? fileprivate var tempFilePath: String? + fileprivate var limitedFileRange: Range? fileprivate var streamable: Bool? fileprivate var statsCategory: MediaResourceStatsCategory? @@ -327,16 +338,22 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.autosaveDisposable.dispose() } - func initializeState(postbox: Postbox, userLocation: MediaResourceUserLocation, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int?, storeAfterDownload: (() -> Void)?) { + func initializeState(postbox: Postbox, userLocation: MediaResourceUserLocation, resourceReference: MediaResourceReference, tempFilePath: String?, limitedFileRange: Range?, streamable: Bool, isSeekable: Bool, video: Bool, preferSoftwareDecoding: Bool, fetchAutomatically: Bool, maximumFetchSize: Int?, storeAfterDownload: (() -> Void)?) { if self.readingError || self.initializedState != nil { return } let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals + var streamable = streamable + if limitedFileRange != nil { + streamable = true + } + self.postbox = postbox self.resourceReference = resourceReference self.tempFilePath = tempFilePath + self.limitedFileRange = limitedFileRange self.streamable = streamable self.statsCategory = video ? .video : .audio self.userLocation = userLocation @@ -381,7 +398,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { } if streamable { - if self.tempFilePath == nil { + if self.tempFilePath == nil && limitedFileRange == nil { self.fetchedDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, range: (0 ..< Int64.max, .elevated), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } } else if !self.requestedCompleteFetch && self.fetchAutomatically { @@ -429,6 +446,14 @@ final class FFMpegMediaFrameSourceContext: NSObject { duration = CMTimeMake(value: Int64.min, timescale: duration.timescale) } + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } + let metrics = avFormatContext.metricsForStream(at: streamIndex) let rotationAngle: Double = metrics.rotationAngle @@ -439,24 +464,24 @@ final class FFMpegMediaFrameSourceContext: NSObject { let codecContext = FFMpegAVCodecContext(codec: codec) if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { if codecContext.open() { - videoStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) + videoStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) break } } } } else if codecId == FFMpegCodecIdMPEG4 { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromMpeg4CodecData(UInt32(kCMVideoCodecType_MPEG4Video), metrics.width, metrics.height, metrics.extradata, metrics.extradataSize) { - videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) + videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) break } } else if codecId == FFMpegCodecIdH264 { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromAVCCodecData(UInt32(kCMVideoCodecType_H264), metrics.width, metrics.height, metrics.extradata, metrics.extradataSize) { - videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) + videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) break } } else if codecId == FFMpegCodecIdHEVC { if let videoFormat = FFMpegMediaFrameSourceContextHelpers.createFormatDescriptionFromHEVCCodecData(UInt32(kCMVideoCodecType_HEVC), metrics.width, metrics.height, metrics.extradata, metrics.extradataSize) { - videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) + videoStream = StreamContext(index: Int(streamIndex), codecContext: nil, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaPassthroughVideoFrameDecoder(videoFormat: videoFormat, rotationAngle: rotationAngle), rotationAngle: rotationAngle, aspect: aspect) break } } @@ -484,7 +509,15 @@ final class FFMpegMediaFrameSourceContext: NSObject { duration = CMTimeMake(value: Int64.min, timescale: duration.timescale) } - audioStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0, aspect: 1.0) + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } + + audioStream = StreamContext(index: Int(streamIndex), codecContext: codecContext, fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegAudioFrameDecoder(codecContext: codecContext), rotationAngle: 0.0, aspect: 1.0) break } } @@ -493,7 +526,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.initializedState = InitializedState(avIoContext: avIoContext, avFormatContext: avFormatContext, audioStream: audioStream, videoStream: videoStream) - if streamable { + if streamable && limitedFileRange == nil { if self.tempFilePath == nil { self.fetchedFullDataDisposable.set(fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: self.userContentType ?? .other, reference: resourceReference, range: (0 ..< Int64.max, .default), statsCategory: self.statsCategory ?? .generic, preferBackgroundReferenceRevalidation: streamable).start()) } @@ -620,11 +653,11 @@ final class FFMpegMediaFrameSourceContext: NSObject { var videoDescription: FFMpegMediaFrameSourceDescription? if let audioStream = initializedState.audioStream { - audioDescription = FFMpegMediaFrameSourceDescription(duration: audioStream.duration, decoder: audioStream.decoder, rotationAngle: 0.0, aspect: 1.0) + audioDescription = FFMpegMediaFrameSourceDescription(startTime: audioStream.startTime, duration: audioStream.duration, decoder: audioStream.decoder, rotationAngle: 0.0, aspect: 1.0) } if let videoStream = initializedState.videoStream { - videoDescription = FFMpegMediaFrameSourceDescription(duration: videoStream.duration, decoder: videoStream.decoder, rotationAngle: videoStream.rotationAngle, aspect: videoStream.aspect) + videoDescription = FFMpegMediaFrameSourceDescription(startTime: videoStream.startTime, duration: videoStream.duration, decoder: videoStream.decoder, rotationAngle: videoStream.rotationAngle, aspect: videoStream.aspect) } var actualPts: CMTime = CMTimeMake(value: 0, timescale: 1) diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index 20ec8f5f5d..c2e23f6743 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -117,6 +117,7 @@ private final class MediaPlayerContext { private let userContentType: MediaResourceUserContentType private let resourceReference: MediaResourceReference private let tempFilePath: String? + private let limitedFileRange: Range? private let streamable: MediaPlayerStreaming private let video: Bool private let preferSoftwareDecoding: Bool @@ -151,7 +152,7 @@ private final class MediaPlayerContext { private var stoppedAtEnd = false - init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, limitedFileRange: Range?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool) { assert(queue.isCurrent()) self.queue = queue @@ -163,6 +164,7 @@ private final class MediaPlayerContext { self.userContentType = userContentType self.resourceReference = resourceReference self.tempFilePath = tempFilePath + self.limitedFileRange = limitedFileRange self.streamable = streamable self.video = video self.preferSoftwareDecoding = preferSoftwareDecoding @@ -232,7 +234,7 @@ private final class MediaPlayerContext { if let loadedState = maybeLoadedState, let videoBuffer = loadedState.mediaBuffers.videoBuffer { if let (extraVideoFrames, atTime) = loadedState.extraVideoFrames { loadedState.extraVideoFrames = nil - return .restoreState(extraVideoFrames, atTime) + return .restoreState(frames: extraVideoFrames, atTimestamp: atTime, soft: false) } else { return videoBuffer.takeFrame() } @@ -340,7 +342,7 @@ private final class MediaPlayerContext { let _ = self.playerStatusValue.swap(status) } - let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, userLocation: self.userLocation, userContentType: self.userContentType, resourceReference: self.resourceReference, tempFilePath: self.tempFilePath, streamable: self.streamable.enabled, isSeekable: self.streamable.isSeekable, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically, stallDuration: self.streamable.parameters.0, lowWaterDuration: self.streamable.parameters.1, highWaterDuration: self.streamable.parameters.2, storeAfterDownload: self.storeAfterDownload) + let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, userLocation: self.userLocation, userContentType: self.userContentType, resourceReference: self.resourceReference, tempFilePath: self.tempFilePath, limitedFileRange: self.limitedFileRange, streamable: self.streamable.enabled, isSeekable: self.streamable.isSeekable, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically, stallDuration: self.streamable.parameters.0, lowWaterDuration: self.streamable.parameters.1, highWaterDuration: self.streamable.parameters.2, storeAfterDownload: self.storeAfterDownload) let disposable = MetaDisposable() let updatedSeekState: MediaPlayerSeekState? if let loadedDuration = loadedDuration { @@ -1128,10 +1130,10 @@ public final class MediaPlayer { } } - public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, soundMuted: Bool = false, ambient: Bool = false, mixWithOthers: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool = false) { + public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String? = nil, limitedFileRange: Range? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, soundMuted: Bool = false, ambient: Bool = false, mixWithOthers: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool = false) { let audioLevelPipe = self.audioLevelPipe self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, soundMuted: soundMuted, ambient: ambient, mixWithOthers: mixWithOthers, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, soundMuted: soundMuted, ambient: ambient, mixWithOthers: mixWithOthers, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift index 08be7b2aca..b98e506f65 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift @@ -96,7 +96,12 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U if !didSetRate { context.state = .playing(rate: rate, didSetRate: true) let masterClock = CMTimebaseCopySource(context.timebase) - CMTimebaseSetRateAndAnchorTime(context.timebase, rate: rate, anchorTime: CMTimeMake(value: sampleIndex, timescale: 44100), immediateSourceTime: CMSyncGetTime(masterClock)) + let anchorTime = CMTimeMake(value: sampleIndex, timescale: 44100) + let immediateSourceTime = CMSyncGetTime(masterClock) + if anchorTime.seconds < CMTimebaseGetTime(context.timebase).seconds - 0.5 { + assert(true) + } + CMTimebaseSetRateAndAnchorTime(context.timebase, rate: rate, anchorTime: anchorTime, immediateSourceTime: immediateSourceTime) updatedRate = context.updatedRate } else { context.renderTimestampTick += 1 @@ -165,6 +170,10 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U break } } + } else { + #if DEBUG + print("No audio data") + #endif } if !context.notifiedLowWater { diff --git a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift index 76e2c06b36..c9ba67c633 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift @@ -179,13 +179,16 @@ public final class MediaPlayerNode: ASDisplayNode { var state = state takeFrameQueue.async { [weak node] in - switch takeFrame() { - case let .restoreState(frames, atTime): - Queue.mainQueue().async { - guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else { - return + let takeFrameResult = takeFrame() + switch takeFrameResult { + case let .restoreState(frames, atTime, soft): + if !soft { + Queue.mainQueue().async { + guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.flush() } - videoLayer.flush() } for i in 0 ..< frames.count { let frame = frames[i] @@ -193,15 +196,17 @@ public final class MediaPlayerNode: ASDisplayNode { state.maxTakenTime = frameTime let attachments = CMSampleBufferGetSampleAttachmentsArray(frame.sampleBuffer, createIfNecessary: true)! as NSArray let dict = attachments[0] as! NSMutableDictionary - if i == 0 { + if i == 0 && !soft { CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) } - if CMTimeCompare(frame.position, atTime) < 0 { - dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String) - } else if CMTimeCompare(frame.position, atTime) == 0 { - dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) - dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString as String) + if !soft { + if CMTimeCompare(frame.position, atTime) < 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String) + } else if CMTimeCompare(frame.position, atTime) == 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString as String) + } } Queue.mainQueue().async { guard let strongSelf = node, let videoLayer = strongSelf.videoLayer else { diff --git a/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift b/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift index 01f76b8b5f..d23fa4c77f 100644 --- a/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift +++ b/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift @@ -11,7 +11,7 @@ public enum MediaTrackFrameBufferStatus { public enum MediaTrackFrameResult { case noFrames case skipFrame - case restoreState([MediaTrackFrame], CMTime) + case restoreState(frames: [MediaTrackFrame], atTimestamp: CMTime, soft: Bool) case frame(MediaTrackFrame) case finished } @@ -32,6 +32,7 @@ public final class MediaTrackFrameBuffer { private let frameSource: MediaFrameSource private let decoder: MediaTrackFrameDecoder private let type: MediaTrackFrameType + public let startTime: CMTime public let duration: CMTime public let rotationAngle: Double public let aspect: Double @@ -40,16 +41,17 @@ public final class MediaTrackFrameBuffer { private var frameSourceSinkIndex: Int? - private var frames: [MediaTrackDecodableFrame] = [] + private(set) var frames: [MediaTrackDecodableFrame] = [] private var maxFrameTime: Double? private var endOfStream = false private var bufferedUntilTime: CMTime? private var isWaitingForLowWaterDuration: Bool = false - init(frameSource: MediaFrameSource, decoder: MediaTrackFrameDecoder, type: MediaTrackFrameType, duration: CMTime, rotationAngle: Double, aspect: Double, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0) { + init(frameSource: MediaFrameSource, decoder: MediaTrackFrameDecoder, type: MediaTrackFrameType, startTime: CMTime, duration: CMTime, rotationAngle: Double, aspect: Double, stallDuration: Double = 1.0, lowWaterDuration: Double = 2.0, highWaterDuration: Double = 3.0) { self.frameSource = frameSource self.type = type self.decoder = decoder + self.startTime = startTime self.duration = duration self.rotationAngle = rotationAngle self.aspect = aspect @@ -192,8 +194,10 @@ public final class MediaTrackFrameBuffer { if self.endOfStream, let decodedFrame = self.decoder.takeRemainingFrame() { return .frame(decodedFrame) } else { - if let bufferedUntilTime = self.bufferedUntilTime { - if CMTimeCompare(bufferedUntilTime, self.duration) >= 0 || self.endOfStream { + if self.endOfStream { + return .finished + } else if let bufferedUntilTime = self.bufferedUntilTime { + if CMTimeCompare(bufferedUntilTime, self.duration) >= 0 { return .finished } } diff --git a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift index 0ac0e1c56c..8d919864c6 100644 --- a/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/SoftwareVideoSource.swift @@ -38,15 +38,17 @@ private final class SoftwareVideoStream { let index: Int let fps: CMTime let timebase: CMTime + let startTime: CMTime let duration: CMTime let decoder: FFMpegMediaVideoFrameDecoder let rotationAngle: Double let aspect: Double - init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) { + init(index: Int, fps: CMTime, timebase: CMTime, startTime: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) { self.index = index self.fps = fps self.timebase = timebase + self.startTime = startTime self.duration = duration self.decoder = decoder self.rotationAngle = rotationAngle @@ -126,6 +128,13 @@ public final class SoftwareVideoSource { let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) let metrics = avFormatContext.metricsForStream(at: streamIndex) @@ -137,7 +146,7 @@ public final class SoftwareVideoSource { let codecContext = FFMpegAVCodecContext(codec: codec) if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { if codecContext.open() { - videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) + videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, startTime: startTime, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) break } } @@ -222,6 +231,13 @@ public final class SoftwareVideoSource { } } + public func readTrackInfo() -> (offset: CMTime, duration: CMTime)? { + guard let videoStream = self.videoStream else { + return nil + } + return (videoStream.startTime, CMTimeMaximum(CMTime(value: 0, timescale: videoStream.duration.timescale), CMTimeSubtract(videoStream.duration, videoStream.startTime))) + } + public func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) { guard let videoStream = self.videoStream, let avFormatContext = self.avFormatContext else { return (nil, 0.0, 1.0, false) @@ -490,3 +506,113 @@ public final class SoftwareAudioSource { } } } + +public final class FFMpegMediaInfo { + public let startTime: CMTime + public let duration: CMTime + + public init(startTime: CMTime, duration: CMTime) { + self.startTime = startTime + self.duration = duration + } +} + +private final class FFMpegMediaInfoExtractContext { + let fd: Int32 + let size: Int + + init(fd: Int32, size: Int) { + self.fd = fd + self.size = size + } +} + +private func FFMpegMediaInfoExtractContextReadPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + let result = read(context.fd, buffer, Int(bufferSize)) + if result == 0 { + return FFMPEG_CONSTANT_AVERROR_EOF + } + return Int32(result) +} + +private func FFMpegMediaInfoExtractContextSeekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + if (whence & FFMPEG_AVSEEK_SIZE) != 0 { + return Int64(context.size) + } else { + lseek(context.fd, off_t(offset), SEEK_SET) + return offset + } +} + +public func extractFFMpegMediaInfo(path: String) -> FFMpegMediaInfo? { + let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals + + var s = stat() + stat(path, &s) + let size = Int32(s.st_size) + + let fd = open(path, O_RDONLY, S_IRUSR) + if fd < 0 { + return nil + } + defer { + close(fd) + } + + let avFormatContext = FFMpegAVFormatContext() + let ioBufferSize = 64 * 1024 + + let context = FFMpegMediaInfoExtractContext(fd: fd, size: Int(size)) + + guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(context).toOpaque(), readPacket: FFMpegMediaInfoExtractContextReadPacketCallback, writePacket: nil, seek: FFMpegMediaInfoExtractContextSeekCallback, isSeekable: true) else { + return nil + } + + avFormatContext.setIO(avIoContext) + + if !avFormatContext.openInput() { + return nil + } + + if !avFormatContext.findStreamInfo() { + return nil + } + + var streamInfos: [(isVideo: Bool, info: FFMpegMediaInfo)] = [] + + for typeIndex in 0 ..< 1 { + let isVideo = typeIndex == 0 + + for streamIndexNumber in avFormatContext.streamIndices(for: isVideo ? FFMpegAVFormatStreamTypeVideo : FFMpegAVFormatStreamTypeAudio) { + let streamIndex = streamIndexNumber.int32Value + if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) { + continue + } + + let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) + let (_, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) + + let startTime: CMTime + let rawStartTime = avFormatContext.startTime(atStreamIndex: streamIndex) + if rawStartTime == Int64(bitPattern: 0x8000000000000000 as UInt64) { + startTime = CMTime(value: 0, timescale: timebase.timescale) + } else { + startTime = CMTimeMake(value: rawStartTime, timescale: timebase.timescale) + } + var duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) + duration = CMTimeMaximum(CMTime(value: 0, timescale: duration.timescale), CMTimeSubtract(duration, startTime)) + + streamInfos.append((isVideo: isVideo, info: FFMpegMediaInfo(startTime: startTime, duration: duration))) + } + } + + if let video = streamInfos.first(where: \.isVideo) { + return video.info + } else if let stream = streamInfos.first { + return stream.info + } else { + return nil + } +} diff --git a/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift b/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift index 58109e5feb..a94c48e326 100644 --- a/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift +++ b/submodules/MediaPlayer/Sources/TimeBasedVideoPreload.swift @@ -15,8 +15,7 @@ public func preloadVideoResource(postbox: Postbox, userLocation: MediaResourceUs let disposable = MetaDisposable() queue.async { let maximumFetchSize = 2 * 1024 * 1024 + 128 * 1024 - //let maximumFetchSize = 128 - let sourceImpl = FFMpegMediaFrameSource(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: nil, streamable: true, isSeekable: true, video: true, preferSoftwareDecoding: false, fetchAutomatically: true, maximumFetchSize: maximumFetchSize) + let sourceImpl = FFMpegMediaFrameSource(queue: queue, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: nil, limitedFileRange: nil, streamable: true, isSeekable: true, video: true, preferSoftwareDecoding: false, fetchAutomatically: true, maximumFetchSize: maximumFetchSize) let source = QueueLocalObject(queue: queue, generate: { return sourceImpl }) diff --git a/submodules/MediaPlayer/Sources/VideoPlayerProxy.swift b/submodules/MediaPlayer/Sources/VideoPlayerProxy.swift index 40ba6290e9..6e6196cd11 100644 --- a/submodules/MediaPlayer/Sources/VideoPlayerProxy.swift +++ b/submodules/MediaPlayer/Sources/VideoPlayerProxy.swift @@ -114,4 +114,12 @@ final class VideoPlayerProxy { nodeRef.release() } } + + func flush() { + self.withContext { context in + if let context = context { + context.node?.reset() + } + } + } } diff --git a/submodules/MtProtoKit/Sources/PingFoundation.h b/submodules/MtProtoKit/Sources/PingFoundation.h index 9c6d5a0234..07041a0c7b 100644 --- a/submodules/MtProtoKit/Sources/PingFoundation.h +++ b/submodules/MtProtoKit/Sources/PingFoundation.h @@ -8,8 +8,6 @@ #include -#import - #pragma mark * PingFoundation @protocol PingFoundationDelegate; diff --git a/submodules/MtProtoKit/Sources/PingFoundation.m b/submodules/MtProtoKit/Sources/PingFoundation.m index f605c272e6..dfb6df3924 100644 --- a/submodules/MtProtoKit/Sources/PingFoundation.m +++ b/submodules/MtProtoKit/Sources/PingFoundation.m @@ -138,8 +138,8 @@ static uint16_t in_cksum(const void *buffer, size_t bufferLen) { [self stop]; } -- (sa_family_t)hostAddressFamily { - sa_family_t result; +- (__uint8_t)hostAddressFamily { + __uint8_t result; result = AF_UNSPEC; if ( (self.hostAddress != nil) && (self.hostAddress.length >= sizeof(struct sockaddr)) ) diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 39f2a74a06..b0173828c7 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -448,14 +448,22 @@ private func chatMessageImageFileThumbnailDatas(account: Account, userLocation: return signal } -private func chatMessageVideoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, fileReference: FileMediaReference, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal?, Bool>, NoError> { +private func chatMessageVideoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference?, thumbnailSize: Bool = false, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, forceThumbnail: Bool = false) -> Signal?, Bool>, NoError> { let fullSizeResource = fileReference.media.resource var reducedSizeResource: MediaResource? - if let videoThumbnail = fileReference.media.videoThumbnails.first { + if let previewSourceFileReference, let videoThumbnail = previewSourceFileReference.media.videoThumbnails.first { + reducedSizeResource = videoThumbnail.resource + } else if let videoThumbnail = fileReference.media.videoThumbnails.first { reducedSizeResource = videoThumbnail.resource } - let thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(fileReference.media.previewRepresentations) : smallestImageRepresentation(fileReference.media.previewRepresentations) + var thumbnailRepresentation: TelegramMediaImageRepresentation? + if let previewSourceFileReference { + thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(previewSourceFileReference.media.previewRepresentations) : smallestImageRepresentation(previewSourceFileReference.media.previewRepresentations) + } + if thumbnailRepresentation == nil { + thumbnailRepresentation = useLargeThumbnail ? largestImageRepresentation(fileReference.media.previewRepresentations) : smallestImageRepresentation(fileReference.media.previewRepresentations) + } let thumbnailResource = thumbnailRepresentation?.resource let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false, attemptSynchronously: synchronousLoad) @@ -979,7 +987,7 @@ public func chatMessagePhotoThumbnail(account: Account, userLocation: MediaResou } public func chatMessageVideoThumbnail(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, blurred: Bool = false, synchronousLoads: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, thumbnailSize: true, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred) + let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, previewSourceFileReference: nil, thumbnailSize: true, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred) return signal |> map { value in @@ -1581,7 +1589,7 @@ public func mediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceU } } -public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { +public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, videoReference: FileMediaReference, previewSourceFileReference: FileMediaReference? = nil, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, useLargeThumbnail: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false, useMiniThumbnailIfAvailable: Bool = false, blurred: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> { let signal: Signal?, Bool>, NoError> if let imageReference = imageReference { signal = chatMessagePhotoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad, forceThumbnail: blurred) @@ -1592,7 +1600,7 @@ public func internalMediaGridMessageVideo(postbox: Postbox, userLocation: MediaR return Tuple(thumbnailData, fullSizeData.flatMap({ Tuple($0, "") }), fullSizeComplete) } } else { - signal = chatMessageVideoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, fileReference: videoReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, forceThumbnail: blurred) + signal = chatMessageVideoDatas(postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, fileReference: videoReference, previewSourceFileReference: previewSourceFileReference, onlyFullSize: onlyFullSize, useLargeThumbnail: useLargeThumbnail, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, forceThumbnail: blurred) } return signal @@ -3146,3 +3154,20 @@ public func callDefaultBackground() -> Signal<(TransformImageArguments) -> Drawi return context }) } + +public func solidColorImage(_ color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return .single({ arguments in + guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { + return nil + } + + context.withFlippedContext { c in + c.setFillColor(color.withAlphaComponent(1.0).cgColor) + c.fill(arguments.drawingRect) + } + + addCorners(context, arguments: arguments) + + return context + }) +} diff --git a/submodules/Postbox/Sources/FileSize.swift b/submodules/Postbox/Sources/FileSize.swift index 3740d7b142..ef4fc4863a 100644 --- a/submodules/Postbox/Sources/FileSize.swift +++ b/submodules/Postbox/Sources/FileSize.swift @@ -1,19 +1,27 @@ import Foundation public func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { - if useTotalFileAllocatedSize { + /*if useTotalFileAllocatedSize { let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) { + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { if values.isRegularFile ?? false { - if let fileSize = values.totalFileAllocatedSize { + if let fileSize = values.fileAllocatedSize { return Int64(fileSize) } } } - } + }*/ var value = stat() - if stat(path, &value) == 0 { + if lstat(path, &value) == 0 { + if (value.st_mode & S_IFMT) == S_IFLNK { + return 0 + } + + if useTotalFileAllocatedSize { + return Int64(value.st_blocks) * Int64(value.st_blksize) + } + return value.st_size } else { return nil diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift index ff73495535..339c9da479 100644 --- a/submodules/Postbox/Sources/StorageBox/StorageBox.swift +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -18,6 +18,8 @@ private func md5Hash(_ data: Data) -> HashId { return HashId(data: hashData) } +public var storageBoxDebugFunc: ((Int64) -> Void)? + public final class StorageBox { public final class Stats { public final class ContentTypeStats { @@ -320,7 +322,11 @@ public final class StorageBox { currentSize = 0 } - self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false)) + withExtendedLifetime(key, { + withUnsafeMutablePointer(to: ¤tSize, { pointer in + self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: UnsafeMutableRawPointer(pointer), capacity: 8, length: 8, freeWhenDone: false)) + }) + }) self.totalSize += delta } @@ -342,7 +348,11 @@ public final class StorageBox { currentSize = 0 } - self.valueBox.set(self.peerContentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false)) + withExtendedLifetime(key, { + withUnsafeMutablePointer(to: ¤tSize, { pointer in + self.valueBox.set(self.peerContentTypeStatsTable, key: key, value: MemoryBuffer(memory: UnsafeMutableRawPointer(pointer), capacity: 8, length: 8, freeWhenDone: false)) + }) + }) } func internalAdd(reference: Reference, to id: Data, contentType: UInt8, size: Int64?) { diff --git a/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift index b97d6453df..874fac42f4 100644 --- a/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift +++ b/submodules/Postbox/Sources/Utils/Encoder/AdaptedPostboxUnkeyedEncodingContainer.swift @@ -119,7 +119,7 @@ extension _AdaptedPostboxEncoder.UnkeyedContainer: UnkeyedEncodingContainer { } else if value is String { try self.encode(value as! String) } else if value is Data { - try self.encode(value as! Data) + try self.encodeData(value as! Data) } else if let value = value as? AdaptedPostboxEncoder.RawObjectData { let buffer = WriteBuffer() @@ -174,7 +174,7 @@ extension _AdaptedPostboxEncoder.UnkeyedContainer: UnkeyedEncodingContainer { } private extension _AdaptedPostboxEncoder.UnkeyedContainer { - func encode(_ value: Data) throws { + func encodeData(_ value: Data) throws { self.items.append(.data(value)) } } diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index b878cc81f9..e77b52af64 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -784,7 +784,6 @@ private func createGiveawayControllerEntries( entries.append(.prepaid(presentationData.theme, title, text, prepaidGiveaway)) } - var starsPerUser: Int64 = 0 if case .generic = subject, case .starsGiveaway = state.mode, !starsGiveawayOptions.isEmpty { let selectedOption = starsGiveawayOptions.first(where: { $0.giveawayOption.count == state.stars })! entries.append(.starsHeader(presentationData.theme, presentationData.strings.BoostGift_Stars_Title.uppercased(), presentationData.strings.BoostGift_Stars_Boosts(selectedOption.giveawayOption.yearlyBoosts).uppercased())) @@ -795,13 +794,16 @@ private func createGiveawayControllerEntries( continue } let giftTitle: String = presentationData.strings.BoostGift_Stars_Stars(Int32(product.giveawayOption.count)) - let winners = product.giveawayOption.winners.first(where: { $0.users == state.winners }) ?? product.giveawayOption.winners.first! - let maxWinners = product.giveawayOption.winners.sorted(by: { $0.users < $1.users }).last?.users ?? 1 - - let subtitle = presentationData.strings.BoostGift_Stars_PerUser("\(winners.starsPerUser)").string + + let starsPerUser: Int64 + if let winners = product.giveawayOption.winners.first(where: { $0.users == state.winners }) { + starsPerUser = winners.starsPerUser + } else { + starsPerUser = product.giveawayOption.count / Int64(state.winners) + } + let subtitle = presentationData.strings.BoostGift_Stars_PerUser("\(starsPerUser)").string let label = product.storeProduct.price - starsPerUser = winners.starsPerUser let isSelected = product.giveawayOption.count == state.stars entries.append(.stars(i, presentationData.theme, Int32(product.giveawayOption.count), giftTitle, subtitle, label, isSelected, maxWinners)) @@ -941,7 +943,6 @@ private func createGiveawayControllerEntries( if state.mode == .starsGiveaway { let starsString = presentationData.strings.BoostGift_AdditionalPrizesInfoStars(Int32(state.stars)) if state.prizeDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - let _ = starsPerUser prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoStarsOn(starsString, "").string } else { prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoStarsOn(starsString, presentationData.strings.BoostGift_AdditionalPrizesInfoStarsAndOther("\(state.winners)", state.prizeDescription).string).string diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 50bea98f1a..c6d310bcb7 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -1034,7 +1034,8 @@ private final class SheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1, - highlightColor: linkColor.withAlphaComponent(0.2), + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { _ in return nil }, diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index af760b9b64..036c6ce857 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -625,7 +625,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { if let signal = signal { let _ = (signal |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in controller?.push(c) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 6240ee08ca..42c7996161 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2572,7 +2572,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { footer: AnyComponent(MultilineTextComponent( text: .plain(adsInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -2735,7 +2736,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if let signal = signal { let _ = (signal |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in controller?.push(c) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/PresentationDataUtils/Sources/AlertTheme.swift b/submodules/PresentationDataUtils/Sources/AlertTheme.swift index 610480dfc7..1fa7f48d18 100644 --- a/submodules/PresentationDataUtils/Sources/AlertTheme.swift +++ b/submodules/PresentationDataUtils/Sources/AlertTheme.swift @@ -5,11 +5,11 @@ import AccountContext import SwiftSignalKit import TelegramPresentationData -public func textAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true) -> AlertController { - return textAlertController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, forceTheme: forceTheme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap) +public func textAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { + return textAlertController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, forceTheme: forceTheme, title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction) } -public func textAlertController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true) -> AlertController { +public func textAlertController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) -> AlertController { var presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 } if let forceTheme = forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) @@ -21,7 +21,7 @@ public func textAlertController(sharedContext: SharedAccountContext, updatedPres presentationData = presentationData.withUpdated(theme: forceTheme) } return AlertControllerTheme(presentationData: presentationData) - }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap) + }), title: title, text: text, actions: actions, actionLayout: actionLayout, allowInputInset: allowInputInset, parseMarkdown: parseMarkdown, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction) } public func textAlertController(sharedContext: SharedAccountContext, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, dismissOnOutsideTap: Bool = true) -> AlertController { diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 3b8370da8b..21cc6684a0 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -899,7 +899,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, ASScrollVie guard let navigationController = self.controller?.navigationController as? NavigationController else { return false } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { [weak self] peer, navigation in + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { [weak self] peer, navigation in guard let strongSelf = self else { return } diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index bc377a2f1b..df1a0039c9 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -315,7 +315,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo controller?.dismiss() dismissImpl?() - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) @@ -356,7 +356,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo controller?.dismiss() dismissImpl?() - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift index 1736b737a0..88351caec7 100644 --- a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift @@ -82,11 +82,7 @@ private func translationSettingsControllerEntries(theme: PresentationTheme, stri if activeLanguage.hasSuffix(rawSuffix) { activeLanguage = String(activeLanguage.dropLast(rawSuffix.count)) } - if activeLanguage == "nb" { - activeLanguage = "no" - } else if activeLanguage == "pt-br" { - activeLanguage = "pt" - } + activeLanguage = normalizeTranslationLanguage(activeLanguage) selectedLanguages = Set([activeLanguage]) for language in systemLanguageCodes() { selectedLanguages.insert(language) diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index abd5ee4b84..6ec1448740 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -222,7 +222,7 @@ public func logoutOptionsController(context: AccountContext, navigationControlle controller?.dismiss() dismissImpl?() - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in pushControllerImpl?(controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 14baaadd01..9df2804a25 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -1068,7 +1068,7 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList let faq = SettingsSearchableItem(id: .faq(0), title: strings.Settings_FAQ, alternate: synonyms(strings.SettingsSearch_Synonyms_FAQ), icon: .faq, breadcrumbs: [], present: { context, navigationController, present in let _ = (cachedFaqInstantPage(context: context) |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in present(.push, controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index 3b5bbc6448..eb958dece8 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -17,9 +17,9 @@ import AccountContext private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { if useTotalFileAllocatedSize { let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) { + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { if values.isRegularFile ?? false { - if let fileSize = values.totalFileAllocatedSize { + if let fileSize = values.fileAllocatedSize { return Int64(fileSize) } } diff --git a/submodules/StatisticsUI/BUILD b/submodules/StatisticsUI/BUILD index 68533e416a..293b755d92 100644 --- a/submodules/StatisticsUI/BUILD +++ b/submodules/StatisticsUI/BUILD @@ -55,6 +55,7 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", + "//submodules/TelegramUI/Components/PremiumPeerShortcutComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 39a97e73d5..11e1485338 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1167,7 +1167,7 @@ private enum StatsEntry: ItemListNodeEntry { detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) } - let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString label.insert(NSAttributedString(string: " $ ", font: font, textColor: labelColor), at: 1) if let range = label.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: labelColor) { @@ -1559,8 +1559,13 @@ private func monetizationEntries( ) -> [StatsEntry] { var entries: [StatsEntry] = [] + var isBot = false + if case let .user(user) = peer, let _ = user.botInfo { + isBot = true + } + if canViewRevenue { - entries.append(.adsHeader(presentationData.theme, presentationData.strings.Monetization_Header)) + entries.append(.adsHeader(presentationData.theme, isBot ? presentationData.strings.Monetization_Bot_Header : presentationData.strings.Monetization_Header)) if !data.topHoursGraph.isEmpty { entries.append(.adsImpressionsTitle(presentationData.theme, presentationData.strings.Monetization_ImpressionsTitle)) @@ -1602,7 +1607,7 @@ private func monetizationEntries( } if canViewRevenue { - entries.append(.adsTonBalanceTitle(presentationData.theme, presentationData.strings.Monetization_TonBalanceTitle)) + entries.append(.adsTonBalanceTitle(presentationData.theme, isBot ? presentationData.strings.Monetization_Bot_BalanceTitle : presentationData.strings.Monetization_TonBalanceTitle)) entries.append(.adsTonBalance(presentationData.theme, data, isCreator && data.balances.availableBalance > 0, data.balances.withdrawEnabled)) if isCreator { @@ -1644,7 +1649,7 @@ private func monetizationEntries( if displayTonTransactions { if !addedTransactionsTabs { - entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_TonTransactions.uppercased())) + entries.append(.adsTransactionsTitle(presentationData.theme, isBot ? presentationData.strings.Monetization_TransactionsTitle.uppercased() : presentationData.strings.Monetization_TonTransactions.uppercased())) } var transactions = transactionsInfo.transactions @@ -1788,7 +1793,15 @@ private func channelStatsControllerEntries( return [] } -public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil) -> ViewController { +public func channelStatsController( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + peerId: PeerId, + section: ChannelStatsSection = .stats, + existingRevenueContext: RevenueStatsContext? = nil, + boostStatus: ChannelBoostStatus? = nil, + boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil +) -> ViewController { let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0), ignoreRepeated: true) let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false, starsSelected: false, transactionsExpanded: false, moreTransactionsDisplayed: 0)) let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in @@ -1845,7 +1858,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false) let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true) - let revenueContext = RevenueStatsContext(account: context.account, peerId: peerId) + let revenueContext = existingRevenueContext ?? RevenueStatsContext(account: context.account, peerId: peerId) let revenueState = Promise() revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init))) @@ -2013,7 +2026,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD buyAdsImpl?() }, openMonetizationIntro: { - let controller = MonetizationIntroScreen(context: context, openMore: {}) + let controller = MonetizationIntroScreen(context: context, mode: existingRevenueContext != nil ? .bot : .channel, openMore: {}) pushImpl?(controller) }, openMonetizationInfo: { @@ -2112,7 +2125,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD ) |> deliverOnMainQueue |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in - let (canViewStats, adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData + let (canViewStats, adsRestricted, _, canViewStarsRevenue) = peerData + var canViewRevenue = peerData.2 let _ = canViewStatsValue.swap(canViewStats) @@ -2167,7 +2181,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD var headerItem: BoostHeaderItem? var leftNavigationButton: ItemListNavigationButton? var boostsOnly = false - if section == .boosts { + if existingRevenueContext != nil { + title = .text(presentationData.strings.Stats_TonBotRevenue_Title) + canViewRevenue = true + } else if section == .boosts { title = .text("") let headerTitle = isGroup ? presentationData.strings.GroupBoost_Title : presentationData.strings.ChannelBoost_Title diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index 01839afd73..e92c53bd40 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -176,7 +176,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { var isStars = false if let stats = item.stats as? RevenueStats { - let cryptoValue = formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + let cryptoValue = formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat) amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" } else if let stats = item.stats as? StarsRevenueStats { @@ -417,7 +417,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { allowActionWhenDisabled: false, displaysProgress: false, action: { [weak self] in - guard let self, let item = self.item, item.isEnabled else { + guard let self, let item = self.item else { return } item.buyAdsAction?() diff --git a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift index 31db7b01ed..2dc7e1687b 100644 --- a/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift +++ b/submodules/StatisticsUI/Sources/MonetizationIntroScreen.swift @@ -20,15 +20,18 @@ private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let mode: MonetizationIntroScreen.Mode let openMore: () -> Void let dismiss: () -> Void init( context: AccountContext, + mode: MonetizationIntroScreen.Mode, openMore: @escaping () -> Void, dismiss: @escaping () -> Void ) { self.context = context + self.mode = mode self.openMore = openMore self.dismiss = dismiss } @@ -37,6 +40,9 @@ private final class SheetContent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.mode != rhs.mode { + return false + } return true } @@ -136,7 +142,7 @@ private final class SheetContent: CombinedComponent { let title = title.update( component: BalancedTextComponent( - text: .plain(NSAttributedString(string: strings.Monetization_Intro_Title, font: titleFont, textColor: textColor)), + text: .plain(NSAttributedString(string: component.mode == .bot ? strings.Monetization_Intro_Bot_Title : strings.Monetization_Intro_Title, font: titleFont, textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 @@ -157,7 +163,7 @@ private final class SheetContent: CombinedComponent { component: AnyComponent(ParagraphComponent( title: strings.Monetization_Intro_Ads_Title, titleColor: textColor, - text: strings.Monetization_Intro_Ads_Text, + text: component.mode == .bot ? strings.Monetization_Intro_Bot_Ads_Text : strings.Monetization_Intro_Ads_Text, textColor: secondaryTextColor, iconName: "Ads/Ads", iconColor: linkColor @@ -254,6 +260,8 @@ private final class SheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, + highlightColor: linkColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -341,13 +349,16 @@ private final class SheetContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let mode: MonetizationIntroScreen.Mode let openMore: () -> Void init( context: AccountContext, + mode: MonetizationIntroScreen.Mode, openMore: @escaping () -> Void ) { self.context = context + self.mode = mode self.openMore = openMore } @@ -355,6 +366,9 @@ private final class SheetContainerComponent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.mode != rhs.mode { + return false + } return true } @@ -373,6 +387,7 @@ private final class SheetContainerComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, + mode: context.component.mode, openMore: context.component.openMore, dismiss: { animateOut.invoke(Action { _ in @@ -442,9 +457,15 @@ private final class SheetContainerComponent: CombinedComponent { final class MonetizationIntroScreen: ViewControllerComponentContainer { private let context: AccountContext private var openMore: (() -> Void)? - + + enum Mode: Equatable { + case channel + case bot + } + init( context: AccountContext, + mode: Mode, openMore: @escaping () -> Void ) { self.context = context @@ -454,6 +475,7 @@ final class MonetizationIntroScreen: ViewControllerComponentContainer { context: context, component: SheetContainerComponent( context: context, + mode: mode, openMore: openMore ), navigationBarAppearance: .none, diff --git a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift index 1c8ed37359..c09d2b238e 100644 --- a/submodules/StatisticsUI/Sources/StarsTransactionItem.swift +++ b/submodules/StatisticsUI/Sources/StarsTransactionItem.swift @@ -262,6 +262,13 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode { case .ads: itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Title itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramAds_Subtitle + case .apiLimitExtension: + itemTitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramBotApi_Title + if let floodskipNumber = item.transaction.floodskipNumber { + itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_TelegramBotApi_Messages(floodskipNumber) + } else { + itemSubtitle = nil + } case .unsupported: itemTitle = item.presentationData.strings.Stars_Intro_Transaction_Unsupported_Title itemSubtitle = nil diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 3a59f66013..168dd5459d 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -772,7 +772,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Available, (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -782,7 +782,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.currentBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Current, (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -792,7 +792,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.overallRevenue, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Total, (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -836,7 +836,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Available, (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -846,7 +846,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.currentBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Current, (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -856,7 +856,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.overallRevenue, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Total, (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift index a4be4db91a..61491adb44 100644 --- a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -139,7 +139,7 @@ private final class SheetContent: CombinedComponent { switch component.transaction { case let .proceeds(amount, fromDate, toDate): labelColor = theme.list.itemDisclosureActions.constructive.fillColor - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = "\(stringForMediumCompactDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForMediumCompactDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))" titleString = strings.Monetization_TransactionInfo_Proceeds buttonTitle = strings.Common_OK @@ -147,7 +147,7 @@ private final class SheetContent: CombinedComponent { showPeer = true case let .withdrawal(status, amount, date, provider, _, transactionUrl): labelColor = theme.list.itemDestructiveColor - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.groupingSeparator), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) switch status { @@ -166,7 +166,7 @@ private final class SheetContent: CombinedComponent { case let .refund(amount, date, _): labelColor = theme.list.itemDisclosureActions.constructive.fillColor titleString = strings.Monetization_TransactionInfo_Refund - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) buttonTitle = strings.Common_OK explorerUrl = nil diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index f227846ba4..c9935ec6d8 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -389,15 +389,14 @@ private final class StickerPackContainer: ASDisplayNode { return updatedOffset } - - + let ignoreCache = controller?.ignoreCache ?? false let fetchedStickerPacks: Signal<[LoadedStickerPack], NoError> = combineLatest(stickerPacks.map { packReference in for pack in loadedStickerPacks { if case let .result(info, _, _) = pack, case let .id(id, _) = packReference, info.id.id == id { return .single(pack) } } - return context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: true) + return context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: true, ignoreCache: ignoreCache) }) self.itemsDisposable = combineLatest(queue: Queue.mainQueue(), fetchedStickerPacks, context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))).start(next: { [weak self] contents, peer in @@ -2686,13 +2685,14 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen { private var animatedIn: Bool = false fileprivate var initialIsEditing: Bool = false fileprivate var expandIfNeeded: Bool = false + fileprivate let ignoreCache: Bool let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer let mainActionTitle: String? let actionTitle: String? - + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, @@ -2703,6 +2703,7 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen { actionTitle: String? = nil, isEditing: Bool = false, expandIfNeeded: Bool = false, + ignoreCache: Bool = false, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, @@ -2718,6 +2719,7 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen { self.actionTitle = actionTitle self.initialIsEditing = isEditing self.expandIfNeeded = expandIfNeeded + self.ignoreCache = ignoreCache self.parentNavigationController = parentNavigationController self.sendSticker = sendSticker self.sendEmoji = sendEmoji @@ -2952,6 +2954,7 @@ public func StickerPackScreen( actionTitle: String? = nil, isEditing: Bool = false, expandIfNeeded: Bool = false, + ignoreCache: Bool = false, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? = nil, @@ -2969,6 +2972,7 @@ public func StickerPackScreen( actionTitle: actionTitle, isEditing: isEditing, expandIfNeeded: expandIfNeeded, + ignoreCache: ignoreCache, parentNavigationController: parentNavigationController, sendSticker: sendSticker, sendEmoji: sendEmoji, diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index 04b67e4e6f..d0b8d37680 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -18,14 +18,28 @@ final class TabBarControllerNode: ASDisplayNode { private let toolbarActionSelected: (ToolbarActionOption) -> Void private let disabledPressed: () -> Void - var currentControllerNode: ASDisplayNode? { - didSet { - oldValue?.removeFromSupernode() - - if let currentControllerNode = self.currentControllerNode { + var currentControllerNode: ASDisplayNode? + + func setCurrentControllerNode(_ node: ASDisplayNode?) -> () -> Void { + guard node !== self.currentControllerNode else { + return {} + } + + let previousNode = self.currentControllerNode + self.currentControllerNode = node + if let currentControllerNode = self.currentControllerNode { + if let previousNode { + self.insertSubnode(currentControllerNode, aboveSubnode: previousNode) + } else { self.insertSubnode(currentControllerNode, at: 0) } } + + return { [weak self, weak previousNode] in + if previousNode !== self?.currentControllerNode { + previousNode?.removeFromSupernode() + } + } } init(theme: TabBarControllerTheme, navigationBarPresentationData: NavigationBarPresentationData, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingNode, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index 1579700413..13b171ace0 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -111,10 +111,10 @@ open class TabBarControllerImpl: ViewController, TabBarController { } } set(value) { let index = max(0, min(self.controllers.count - 1, value)) - if _selectedIndex != index { - _selectedIndex = index + if self._selectedIndex != index { + self._selectedIndex = index - self.updateSelectedIndex() + self.updateSelectedIndex(animated: true) } } } @@ -340,15 +340,20 @@ open class TabBarControllerImpl: ViewController, TabBarController { public func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { let alpha = max(0.0, min(1.0, alpha)) - transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.backgroundNode, alpha: alpha, delay: 0.15) - transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.separatorNode, alpha: alpha, delay: 0.15) + transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.backgroundNode, alpha: alpha, delay: 0.1) + transition.updateAlpha(node: self.tabBarControllerNode.tabBarNode.separatorNode, alpha: alpha, delay: 0.1) } - private func updateSelectedIndex() { + private func updateSelectedIndex(animated: Bool = false) { if !self.isNodeLoaded { return } + var animated = animated + if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + animated = false + } + var tabBarSelectedIndex = self.selectedIndex if let (cameraItem, _) = self.cameraItemAndAction { if let cameraItemIndex = self.tabBarControllerNode.tabBarNode.tabBarItems.firstIndex(where: { $0.item === cameraItem }) { @@ -359,9 +364,21 @@ open class TabBarControllerImpl: ViewController, TabBarController { } self.tabBarControllerNode.tabBarNode.selectedIndex = tabBarSelectedIndex + var transitionSale: CGFloat = 0.998 + if let currentView = self.currentController?.view { + transitionSale = (currentView.frame.height - 3.0) / currentView.frame.height + } if let currentController = self.currentController { currentController.willMove(toParent: nil) - self.tabBarControllerNode.currentControllerNode = nil + //self.tabBarControllerNode.currentControllerNode = nil + + if animated { + currentController.view.layer.animateScale(from: 1.0, to: transitionSale, duration: 0.12, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { completed in + if completed { + currentController.view.layer.removeAllAnimations() + } + }) + } currentController.removeFromParent() currentController.didMove(toParent: nil) @@ -374,13 +391,25 @@ open class TabBarControllerImpl: ViewController, TabBarController { if let currentController = self.currentController { currentController.willMove(toParent: self) - self.tabBarControllerNode.currentControllerNode = currentController.displayNode self.addChild(currentController) + + let commit = self.tabBarControllerNode.setCurrentControllerNode(currentController.displayNode) + if animated { + currentController.view.layer.animateScale(from: transitionSale, to: 1.0, duration: 0.15, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring) + currentController.view.layer.allowsGroupOpacity = true + currentController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { completed in + if completed { + currentController.view.layer.allowsGroupOpacity = false + } + commit() + }) + } else { + commit() + } currentController.didMove(toParent: self) currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle - } else { } if let layout = self.validLayout { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 14becffa38..ab271c55da 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -894,7 +894,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } dict[1301522832] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } - dict[-1365150482] = { return Api.StarGift.parse_starGift($0) } + dict[1237678029] = { return Api.StarGift.parse_starGift($0) } dict[1577421297] = { return Api.StarsGiftOption.parse_starsGiftOption($0) } dict[-1798404822] = { return Api.StarsGiveawayOption.parse_starsGiveawayOption($0) } dict[1411605001] = { return Api.StarsGiveawayWinnersOption.parse_starsGiveawayWinnersOption($0) } @@ -902,8 +902,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1401868056] = { return Api.StarsSubscription.parse_starsSubscription($0) } dict[88173912] = { return Api.StarsSubscriptionPricing.parse_starsSubscriptionPricing($0) } dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) } - dict[178185410] = { return Api.StarsTransaction.parse_starsTransaction($0) } + dict[903148150] = { return Api.StarsTransaction.parse_starsTransaction($0) } dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) } + dict[-110658899] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAPI($0) } dict[1617438738] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAds($0) } dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) } dict[-382740222] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerFragment($0) } @@ -1004,7 +1005,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1576161051] = { return Api.Update.parse_updateDeleteMessages($0) } dict[1407644140] = { return Api.Update.parse_updateDeleteQuickReply($0) } dict[1450174413] = { return Api.Update.parse_updateDeleteQuickReplyMessages($0) } - dict[-1870238482] = { return Api.Update.parse_updateDeleteScheduledMessages($0) } + dict[-223929981] = { return Api.Update.parse_updateDeleteScheduledMessages($0) } dict[654302845] = { return Api.Update.parse_updateDialogFilter($0) } dict[-1512627963] = { return Api.Update.parse_updateDialogFilterOrder($0) } dict[889491791] = { return Api.Update.parse_updateDialogFilters($0) } diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index a89148bf05..83c674e9d3 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -574,13 +574,13 @@ public extension Api { } public extension Api { enum StarGift: TypeConstructorDescription { - case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, convertStars: Int64) + case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars): + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate): if boxed { - buffer.appendInt32(-1365150482) + buffer.appendInt32(1237678029) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -589,14 +589,16 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityRemains!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt32(availabilityTotal!, buffer: buffer, boxed: false)} serializeInt64(convertStars, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(firstSaleDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(lastSaleDate!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars): - return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("convertStars", convertStars as Any)]) + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let convertStars, let firstSaleDate, let lastSaleDate): + return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any)]) } } @@ -617,6 +619,10 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } var _7: Int64? _7 = reader.readInt64() + var _8: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_8 = reader.readInt32() } + var _9: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_9 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -624,8 +630,10 @@ public extension Api { let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, convertStars: _7!) + let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, convertStars: _7!, firstSaleDate: _8, lastSaleDate: _9) } else { return nil @@ -1002,13 +1010,13 @@ public extension Api { } public extension Api { enum StarsTransaction: TypeConstructorDescription { - case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?) + case starsTransaction(flags: Int32, id: String, stars: Int64, date: Int32, peer: Api.StarsTransactionPeer, title: String?, description: String?, photo: Api.WebDocument?, transactionDate: Int32?, transactionUrl: String?, botPayload: Buffer?, msgId: Int32?, extendedMedia: [Api.MessageMedia]?, subscriptionPeriod: Int32?, giveawayPostId: Int32?, stargift: Api.StarGift?, floodskipNumber: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift): + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipNumber): if boxed { - buffer.appendInt32(178185410) + buffer.appendInt32(903148150) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(id, buffer: buffer, boxed: false) @@ -1030,14 +1038,15 @@ public extension Api { if Int(flags) & Int(1 << 12) != 0 {serializeInt32(subscriptionPeriod!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {serializeInt32(giveawayPostId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 14) != 0 {stargift!.serialize(buffer, true)} + if Int(flags) & Int(1 << 15) != 0 {serializeInt32(floodskipNumber!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift): - return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any)]) + case .starsTransaction(let flags, let id, let stars, let date, let peer, let title, let description, let photo, let transactionDate, let transactionUrl, let botPayload, let msgId, let extendedMedia, let subscriptionPeriod, let giveawayPostId, let stargift, let floodskipNumber): + return ("starsTransaction", [("flags", flags as Any), ("id", id as Any), ("stars", stars as Any), ("date", date as Any), ("peer", peer as Any), ("title", title as Any), ("description", description as Any), ("photo", photo as Any), ("transactionDate", transactionDate as Any), ("transactionUrl", transactionUrl as Any), ("botPayload", botPayload as Any), ("msgId", msgId as Any), ("extendedMedia", extendedMedia as Any), ("subscriptionPeriod", subscriptionPeriod as Any), ("giveawayPostId", giveawayPostId as Any), ("stargift", stargift as Any), ("floodskipNumber", floodskipNumber as Any)]) } } @@ -1082,6 +1091,8 @@ public extension Api { if Int(_1!) & Int(1 << 14) != 0 {if let signature = reader.readInt32() { _16 = Api.parse(reader, signature: signature) as? Api.StarGift } } + var _17: Int32? + if Int(_1!) & Int(1 << 15) != 0 {_17 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1098,8 +1109,9 @@ public extension Api { let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { - return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16) + let _c17 = (Int(_1!) & Int(1 << 15) == 0) || _17 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + return Api.StarsTransaction.starsTransaction(flags: _1!, id: _2!, stars: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16, floodskipNumber: _17) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index a9a831d8b5..950f9bcbf7 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -1,6 +1,7 @@ public extension Api { enum StarsTransactionPeer: TypeConstructorDescription { case starsTransactionPeer(peer: Api.Peer) + case starsTransactionPeerAPI case starsTransactionPeerAds case starsTransactionPeerAppStore case starsTransactionPeerFragment @@ -15,6 +16,12 @@ public extension Api { buffer.appendInt32(-670195363) } peer.serialize(buffer, true) + break + case .starsTransactionPeerAPI: + if boxed { + buffer.appendInt32(-110658899) + } + break case .starsTransactionPeerAds: if boxed { @@ -59,6 +66,8 @@ public extension Api { switch self { case .starsTransactionPeer(let peer): return ("starsTransactionPeer", [("peer", peer as Any)]) + case .starsTransactionPeerAPI: + return ("starsTransactionPeerAPI", []) case .starsTransactionPeerAds: return ("starsTransactionPeerAds", []) case .starsTransactionPeerAppStore: @@ -87,6 +96,9 @@ public extension Api { return nil } } + public static func parse_starsTransactionPeerAPI(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerAPI + } public static func parse_starsTransactionPeerAds(_ reader: BufferReader) -> StarsTransactionPeer? { return Api.StarsTransactionPeer.starsTransactionPeerAds } diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index a758260b09..2c19d44fd9 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -676,7 +676,7 @@ public extension Api { case updateDeleteMessages(messages: [Int32], pts: Int32, ptsCount: Int32) case updateDeleteQuickReply(shortcutId: Int32) case updateDeleteQuickReplyMessages(shortcutId: Int32, messages: [Int32]) - case updateDeleteScheduledMessages(peer: Api.Peer, messages: [Int32]) + case updateDeleteScheduledMessages(flags: Int32, peer: Api.Peer, messages: [Int32], sentMessages: [Int32]?) case updateDialogFilter(flags: Int32, id: Int32, filter: Api.DialogFilter?) case updateDialogFilterOrder(order: [Int32]) case updateDialogFilters @@ -1246,16 +1246,22 @@ public extension Api { serializeInt32(item, buffer: buffer, boxed: false) } break - case .updateDeleteScheduledMessages(let peer, let messages): + case .updateDeleteScheduledMessages(let flags, let peer, let messages, let sentMessages): if boxed { - buffer.appendInt32(-1870238482) + buffer.appendInt32(-223929981) } + serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) buffer.appendInt32(481674261) buffer.appendInt32(Int32(messages.count)) for item in messages { serializeInt32(item, buffer: buffer, boxed: false) } + if Int(flags) & Int(1 << 0) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sentMessages!.count)) + for item in sentMessages! { + serializeInt32(item, buffer: buffer, boxed: false) + }} break case .updateDialogFilter(let flags, let id, let filter): if boxed { @@ -2097,8 +2103,8 @@ public extension Api { return ("updateDeleteQuickReply", [("shortcutId", shortcutId as Any)]) case .updateDeleteQuickReplyMessages(let shortcutId, let messages): return ("updateDeleteQuickReplyMessages", [("shortcutId", shortcutId as Any), ("messages", messages as Any)]) - case .updateDeleteScheduledMessages(let peer, let messages): - return ("updateDeleteScheduledMessages", [("peer", peer as Any), ("messages", messages as Any)]) + case .updateDeleteScheduledMessages(let flags, let peer, let messages, let sentMessages): + return ("updateDeleteScheduledMessages", [("flags", flags as Any), ("peer", peer as Any), ("messages", messages as Any), ("sentMessages", sentMessages as Any)]) case .updateDialogFilter(let flags, let id, let filter): return ("updateDialogFilter", [("flags", flags as Any), ("id", id as Any), ("filter", filter as Any)]) case .updateDialogFilterOrder(let order): @@ -3309,18 +3315,26 @@ public extension Api { } } public static func parse_updateDeleteScheduledMessages(_ reader: BufferReader) -> Update? { - var _1: Api.Peer? + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer + _2 = Api.parse(reader, signature: signature) as? Api.Peer } - var _2: [Int32]? + var _3: [Int32]? if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } + var _4: [Int32]? + if Int(_1!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.Update.updateDeleteScheduledMessages(peer: _1!, messages: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.Update.updateDeleteScheduledMessages(flags: _1!, peer: _2!, messages: _3!, sentMessages: _4) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index ed03072b36..0a6196d7bb 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -2607,23 +2607,6 @@ public extension Api.functions.channels { }) } } -public extension Api.functions.channels { - static func clickSponsoredMessage(flags: Int32, channel: Api.InputChannel, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(21257589) - serializeInt32(flags, buffer: buffer, boxed: false) - channel.serialize(buffer, true) - serializeBytes(randomId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.clickSponsoredMessage", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in - let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool - } - return result - }) - } -} public extension Api.functions.channels { static func convertToGigagroup(channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3165,21 +3148,6 @@ public extension Api.functions.channels { }) } } -public extension Api.functions.channels { - static func getSponsoredMessages(channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-333377601) - channel.serialize(buffer, true) - return (FunctionDescription(name: "channels.getSponsoredMessages", parameters: [("channel", String(describing: channel))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in - let reader = BufferReader(buffer) - var result: Api.messages.SponsoredMessages? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.messages.SponsoredMessages - } - return result - }) - } -} public extension Api.functions.channels { static func inviteToChannel(channel: Api.InputChannel, users: [Api.InputUser]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3344,23 +3312,6 @@ public extension Api.functions.channels { }) } } -public extension Api.functions.channels { - static func reportSponsoredMessage(channel: Api.InputChannel, randomId: Buffer, option: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-1349519687) - channel.serialize(buffer, true) - serializeBytes(randomId, buffer: buffer, boxed: false) - serializeBytes(option, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.reportSponsoredMessage", parameters: [("channel", String(describing: channel)), ("randomId", String(describing: randomId)), ("option", String(describing: option))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.channels.SponsoredMessageReportResult? in - let reader = BufferReader(buffer) - var result: Api.channels.SponsoredMessageReportResult? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.channels.SponsoredMessageReportResult - } - return result - }) - } -} public extension Api.functions.channels { static func restrictSponsoredMessages(channel: Api.InputChannel, restricted: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -3688,22 +3639,6 @@ public extension Api.functions.channels { }) } } -public extension Api.functions.channels { - static func viewSponsoredMessage(channel: Api.InputChannel, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { - let buffer = Buffer() - buffer.appendInt32(-1095836780) - channel.serialize(buffer, true) - serializeBytes(randomId, buffer: buffer, boxed: false) - return (FunctionDescription(name: "channels.viewSponsoredMessage", parameters: [("channel", String(describing: channel)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in - let reader = BufferReader(buffer) - var result: Api.Bool? - if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool - } - return result - }) - } -} public extension Api.functions.chatlists { static func checkChatlistInvite(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -5007,6 +4942,23 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func clickSponsoredMessage(flags: Int32, peer: Api.InputPeer, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(252261477) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeBytes(randomId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.clickSponsoredMessage", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func createChat(flags: Int32, users: [Api.InputUser], title: String, ttlPeriod: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6838,6 +6790,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getSponsoredMessages(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1680673735) + peer.serialize(buffer, true) + return (FunctionDescription(name: "messages.getSponsoredMessages", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.SponsoredMessages? in + let reader = BufferReader(buffer) + var result: Api.messages.SponsoredMessages? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.SponsoredMessages + } + return result + }) + } +} public extension Api.functions.messages { static func getStickerSet(stickerset: Api.InputStickerSet, hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7448,6 +7415,23 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func reportSponsoredMessage(peer: Api.InputPeer, randomId: Buffer, option: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(452189112) + peer.serialize(buffer, true) + serializeBytes(randomId, buffer: buffer, boxed: false) + serializeBytes(option, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.reportSponsoredMessage", parameters: [("peer", String(describing: peer)), ("randomId", String(describing: randomId)), ("option", String(describing: option))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.channels.SponsoredMessageReportResult? in + let reader = BufferReader(buffer) + var result: Api.channels.SponsoredMessageReportResult? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.channels.SponsoredMessageReportResult + } + return result + }) + } +} public extension Api.functions.messages { static func requestAppWebView(flags: Int32, peer: Api.InputPeer, app: Api.InputBotApp, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -8728,6 +8712,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func viewSponsoredMessage(peer: Api.InputPeer, randomId: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1731909873) + peer.serialize(buffer, true) + serializeBytes(randomId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.viewSponsoredMessage", parameters: [("peer", String(describing: peer)), ("randomId", String(describing: randomId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.payments { static func applyGiftCode(slug: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -10103,12 +10103,12 @@ public extension Api.functions.smsjobs { } } public extension Api.functions.stats { - static func getBroadcastRevenueStats(flags: Int32, channel: Api.InputChannel) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getBroadcastRevenueStats(flags: Int32, peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1977595505) + buffer.appendInt32(-142021095) serializeInt32(flags, buffer: buffer, boxed: false) - channel.serialize(buffer, true) - return (FunctionDescription(name: "stats.getBroadcastRevenueStats", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueStats? in + peer.serialize(buffer, true) + return (FunctionDescription(name: "stats.getBroadcastRevenueStats", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueStats? in let reader = BufferReader(buffer) var result: Api.stats.BroadcastRevenueStats? if let signature = reader.readInt32() { @@ -10119,13 +10119,13 @@ public extension Api.functions.stats { } } public extension Api.functions.stats { - static func getBroadcastRevenueTransactions(channel: Api.InputChannel, offset: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getBroadcastRevenueTransactions(peer: Api.InputPeer, offset: Int32, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(6891535) - channel.serialize(buffer, true) + buffer.appendInt32(1889078125) + peer.serialize(buffer, true) serializeInt32(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stats.getBroadcastRevenueTransactions", parameters: [("channel", String(describing: channel)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueTransactions? in + return (FunctionDescription(name: "stats.getBroadcastRevenueTransactions", parameters: [("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueTransactions? in let reader = BufferReader(buffer) var result: Api.stats.BroadcastRevenueTransactions? if let signature = reader.readInt32() { @@ -10136,12 +10136,12 @@ public extension Api.functions.stats { } } public extension Api.functions.stats { - static func getBroadcastRevenueWithdrawalUrl(channel: Api.InputChannel, password: Api.InputCheckPasswordSRP) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func getBroadcastRevenueWithdrawalUrl(peer: Api.InputPeer, password: Api.InputCheckPasswordSRP) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(711323507) - channel.serialize(buffer, true) + buffer.appendInt32(-1644889427) + peer.serialize(buffer, true) password.serialize(buffer, true) - return (FunctionDescription(name: "stats.getBroadcastRevenueWithdrawalUrl", parameters: [("channel", String(describing: channel)), ("password", String(describing: password))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueWithdrawalUrl? in + return (FunctionDescription(name: "stats.getBroadcastRevenueWithdrawalUrl", parameters: [("peer", String(describing: peer)), ("password", String(describing: password))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.BroadcastRevenueWithdrawalUrl? in let reader = BufferReader(buffer) var result: Api.stats.BroadcastRevenueWithdrawalUrl? if let signature = reader.readInt32() { @@ -10809,15 +10809,16 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func searchPosts(flags: Int32, hashtag: String?, area: Api.MediaArea?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func searchPosts(flags: Int32, hashtag: String?, area: Api.MediaArea?, peer: Api.InputPeer?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1827279210) + buffer.appendInt32(-780072697) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(hashtag!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {area!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {peer!.serialize(buffer, true)} serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("area", String(describing: area)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in + return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("area", String(describing: area)), ("peer", String(describing: peer)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in let reader = BufferReader(buffer) var result: Api.stories.FoundStories? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift index 08b7019d97..6ceaf18ebf 100644 --- a/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift +++ b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift @@ -110,9 +110,8 @@ private final class ScheduleVideoChatSheetContentComponent: Component { var contentHeight: CGFloat = 0.0 contentHeight += 16.0 - //TODO:localize let titleString = NSMutableAttributedString() - titleString.append(NSAttributedString(string: "Schedule Video Chat", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + titleString.append(NSAttributedString(string: environment.strings.VideoChat_ScheduleButtonTitle, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) let titleSize = self.title.update( transition: .immediate, @@ -272,7 +271,7 @@ private final class ScheduleVideoChatSheetContentComponent: Component { pressedColor: UIColor(rgb: 0x2B2B2F).withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( - Text(text: "Cancel", font: Font.semibold(17.0), color: environment.theme.list.itemPrimaryTextColor) + Text(text: environment.strings.Common_Cancel, font: Font.semibold(17.0), color: environment.theme.list.itemPrimaryTextColor) )), isEnabled: true, tintWhenDisabled: false, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift index 8170418a48..d50ae11f7f 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedSpeakingToastComponent.swift @@ -105,7 +105,7 @@ final class VideoChatExpandedSpeakingToastComponent: Component { transition: transition, component: AnyComponent(FilledRoundedRectangleComponent( color: UIColor(white: 0.0, alpha: 0.9), - cornerRadius: size.height * 0.5, + cornerRadius: .value(size.height * 0.5), smoothCorners: false )), environment: {}, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index f49f82e495..751c4db8b9 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1224,7 +1224,7 @@ final class VideoChatScreenComponent: Component { )), background: AnyComponent(FilledRoundedRectangleComponent( color: UIColor(white: 1.0, alpha: 0.1), - cornerRadius: navigationButtonDiameter * 0.5, + cornerRadius: .value(navigationButtonDiameter * 0.5), smoothCorners: false )), effectAlignment: .center, diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 3d6d2ebb00..db52366c58 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7098,10 +7098,7 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool { - var useV2 = false - if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_enable_videochatui_v2"] { - useV2 = true - } + var useV2 = true if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { useV2 = false } diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index fbf747c2cf..8b304e8311 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -212,6 +212,7 @@ struct AccountMutableState { var namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]] var updatedOutgoingUniqueMessageIds: [Int64: Int32] var storedStories: [StoryId: UpdatesStoredStory] + var sentScheduledMessageIds: Set var resetForumTopicLists: [PeerId: StateResetForumTopics] = [:] @@ -231,7 +232,7 @@ struct AccountMutableState { var authorizationListUpdated: Bool = false - init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedReplyMessageIds: ReferencedReplyMessageIds, initialReferencedGeneralMessageIds: Set, initialStoredMessages: Set, initialStoredStories: [StoryId: UpdatesStoredStory], initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set]) { + init(initialState: AccountInitialState, initialPeers: [PeerId: Peer], initialReferencedReplyMessageIds: ReferencedReplyMessageIds, initialReferencedGeneralMessageIds: Set, initialStoredMessages: Set, initialStoredStories: [StoryId: UpdatesStoredStory], initialReadInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], initialSentScheduledMessageIds: Set) { self.initialState = initialState self.state = initialState.state self.peers = initialPeers @@ -240,6 +241,7 @@ struct AccountMutableState { self.referencedGeneralMessageIds = initialReferencedGeneralMessageIds self.storedMessages = initialStoredMessages self.storedStories = initialStoredStories + self.sentScheduledMessageIds = initialSentScheduledMessageIds self.readInboxMaxIds = initialReadInboxMaxIds self.channelStates = initialState.channelStates self.peerChatInfos = initialState.peerChatInfos @@ -249,7 +251,7 @@ struct AccountMutableState { self.updatedOutgoingUniqueMessageIds = [:] } - init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { + init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], sentScheduledMessageIds: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { self.initialState = initialState self.operations = operations self.state = state @@ -260,6 +262,7 @@ struct AccountMutableState { self.referencedGeneralMessageIds = referencedGeneralMessageIds self.storedMessages = storedMessages self.storedStories = storedStories + self.sentScheduledMessageIds = sentScheduledMessageIds self.peerChatInfos = peerChatInfos self.readInboxMaxIds = readInboxMaxIds self.storedMessagesByPeerIdAndTimestamp = storedMessagesByPeerIdAndTimestamp @@ -271,7 +274,7 @@ struct AccountMutableState { } func branch() -> AccountMutableState { - return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) + return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, sentScheduledMessageIds: self.sentScheduledMessageIds, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) } mutating func merge(_ other: AccountMutableState) { @@ -282,6 +285,8 @@ struct AccountMutableState { self.storedStories[id] = story } + self.sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds) + for i in other.branchOperationIndex ..< other.operations.count { self.addOperation(other.operations[i]) } @@ -357,6 +362,10 @@ struct AccountMutableState { self.addOperation(.DeleteMessages(messageIds)) } + mutating func addSentScheduledMessageIds(_ messageIds: [MessageId]) { + self.sentScheduledMessageIds.formUnion(messageIds) + } + mutating func editMessage(_ id: MessageId, message: StoreMessage) { self.addOperation(.EditMessage(id, message)) } @@ -842,13 +851,15 @@ struct AccountReplayedFinalState { let updatedRevenueBalances: [PeerId: RevenueStats.Balances] let updatedStarsBalance: [PeerId: Int64] let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] + let sentScheduledMessageIds: Set } struct AccountFinalStateEvents { let addedIncomingMessageIds: [MessageId] let addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] - let wasScheduledMessageIds:[MessageId] + let wasScheduledMessageIds: [MessageId] let deletedMessageIds: [DeletedMessageId] + let sentScheduledMessageIds: Set let updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] let updatedWebpages: [MediaId: TelegramMediaWebpage] let updatedCalls: [Api.PhoneCall] @@ -873,10 +884,10 @@ struct AccountFinalStateEvents { let updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] var isEmpty: Bool { - return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty + return self.addedIncomingMessageIds.isEmpty && self.addedReactionEvents.isEmpty && self.wasScheduledMessageIds.isEmpty && self.deletedMessageIds.isEmpty && self.sentScheduledMessageIds.isEmpty && self.updatedTypingActivities.isEmpty && self.updatedWebpages.isEmpty && self.updatedCalls.isEmpty && self.addedCallSignalingData.isEmpty && self.updatedGroupCallParticipants.isEmpty && self.storyUpdates.isEmpty && self.updatedPeersNearby?.isEmpty ?? true && self.isContactUpdates.isEmpty && self.displayAlerts.isEmpty && self.dismissBotWebViews.isEmpty && self.delayNotificatonsUntil == nil && self.updatedMaxMessageId == nil && self.updatedQts == nil && self.externallyUpdatedPeerId.isEmpty && !authorizationListUpdated && self.updatedIncomingThreadReadStates.isEmpty && self.updatedOutgoingThreadReadStates.isEmpty && !self.updateConfig && !self.isPremiumUpdated && self.updatedRevenueBalances.isEmpty && self.updatedStarsBalance.isEmpty && self.updatedStarsRevenueStatus.isEmpty } - init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: Int64] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:]) { + init(addedIncomingMessageIds: [MessageId] = [], addedReactionEvents: [(reactionAuthor: Peer, reaction: MessageReaction.Reaction, message: Message, timestamp: Int32)] = [], wasScheduledMessageIds: [MessageId] = [], deletedMessageIds: [DeletedMessageId] = [], updatedTypingActivities: [PeerActivitySpace: [PeerId: PeerInputActivity?]] = [:], updatedWebpages: [MediaId: TelegramMediaWebpage] = [:], updatedCalls: [Api.PhoneCall] = [], addedCallSignalingData: [(Int64, Data)] = [], updatedGroupCallParticipants: [(Int64, GroupCallParticipantsContext.Update)] = [], storyUpdates: [InternalStoryUpdate] = [], updatedPeersNearby: [PeerNearby]? = nil, isContactUpdates: [(PeerId, Bool)] = [], displayAlerts: [(text: String, isDropAuth: Bool)] = [], dismissBotWebViews: [Int64] = [], delayNotificatonsUntil: Int32? = nil, updatedMaxMessageId: Int32? = nil, updatedQts: Int32? = nil, externallyUpdatedPeerId: Set = Set(), authorizationListUpdated: Bool = false, updatedIncomingThreadReadStates: [MessageId: MessageId.Id] = [:], updatedOutgoingThreadReadStates: [MessageId: MessageId.Id] = [:], updateConfig: Bool = false, isPremiumUpdated: Bool = false, updatedRevenueBalances: [PeerId: RevenueStats.Balances] = [:], updatedStarsBalance: [PeerId: Int64] = [:], updatedStarsRevenueStatus: [PeerId: StarsRevenueStats.Balances] = [:], sentScheduledMessageIds: Set = Set()) { self.addedIncomingMessageIds = addedIncomingMessageIds self.addedReactionEvents = addedReactionEvents self.wasScheduledMessageIds = wasScheduledMessageIds @@ -903,6 +914,7 @@ struct AccountFinalStateEvents { self.updatedRevenueBalances = updatedRevenueBalances self.updatedStarsBalance = updatedStarsBalance self.updatedStarsRevenueStatus = updatedStarsRevenueStatus + self.sentScheduledMessageIds = sentScheduledMessageIds } init(state: AccountReplayedFinalState) { @@ -932,6 +944,7 @@ struct AccountFinalStateEvents { self.updatedRevenueBalances = state.updatedRevenueBalances self.updatedStarsBalance = state.updatedStarsBalance self.updatedStarsRevenueStatus = state.updatedStarsRevenueStatus + self.sentScheduledMessageIds = state.sentScheduledMessageIds } func union(with other: AccountFinalStateEvents) -> AccountFinalStateEvents { @@ -961,6 +974,9 @@ struct AccountFinalStateEvents { let isPremiumUpdated = self.isPremiumUpdated || other.isPremiumUpdated - return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs })) + var sentScheduledMessageIds = self.sentScheduledMessageIds + sentScheduledMessageIds.formUnion(other.sentScheduledMessageIds) + + return AccountFinalStateEvents(addedIncomingMessageIds: self.addedIncomingMessageIds + other.addedIncomingMessageIds, addedReactionEvents: self.addedReactionEvents + other.addedReactionEvents, wasScheduledMessageIds: self.wasScheduledMessageIds + other.wasScheduledMessageIds, deletedMessageIds: self.deletedMessageIds + other.deletedMessageIds, updatedTypingActivities: self.updatedTypingActivities, updatedWebpages: self.updatedWebpages, updatedCalls: self.updatedCalls + other.updatedCalls, addedCallSignalingData: self.addedCallSignalingData + other.addedCallSignalingData, updatedGroupCallParticipants: self.updatedGroupCallParticipants + other.updatedGroupCallParticipants, storyUpdates: self.storyUpdates + other.storyUpdates, isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, externallyUpdatedPeerId: externallyUpdatedPeerId, authorizationListUpdated: authorizationListUpdated, updatedIncomingThreadReadStates: self.updatedIncomingThreadReadStates.merging(other.updatedIncomingThreadReadStates, uniquingKeysWith: { lhs, _ in lhs }), updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: self.updatedRevenueBalances.merging(other.updatedRevenueBalances, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsBalance: self.updatedStarsBalance.merging(other.updatedStarsBalance, uniquingKeysWith: { lhs, _ in lhs }), updatedStarsRevenueStatus: self.updatedStarsRevenueStatus.merging(other.updatedStarsRevenueStatus, uniquingKeysWith: { lhs, _ in lhs }), sentScheduledMessageIds: sentScheduledMessageIds) } } diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index a362cd626a..44589cb43a 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -186,7 +186,7 @@ private var declaredEncodables: Void = { declareEncodable(CloudPeerPhotoSizeMediaResource.self, f: { CloudPeerPhotoSizeMediaResource(decoder: $0) }) declareEncodable(CloudStickerPackThumbnailMediaResource.self, f: { CloudStickerPackThumbnailMediaResource(decoder: $0) }) declareEncodable(ContentRequiresValidationMessageAttribute.self, f: { ContentRequiresValidationMessageAttribute(decoder: $0) }) - declareEncodable(WasScheduledMessageAttribute.self, f: { WasScheduledMessageAttribute(decoder: $0) }) + declareEncodable(PendingProcessingMessageAttribute.self, f: { PendingProcessingMessageAttribute(decoder: $0) }) declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) }) declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) }) declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(decoder: $0) }) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 3fa705177b..1b650fa415 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -655,6 +655,12 @@ extension StoreMessage { convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, factCheck): + var attributes: [MessageAttribute] = [] + + if (flags2 & (1 << 4)) != 0 { + attributes.append(PendingProcessingMessageAttribute(approximateCompletionTime: date)) + } + let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId var namespace = namespace @@ -676,8 +682,6 @@ extension StoreMessage { authorId = resolvedFromId } - var attributes: [MessageAttribute] = [] - var threadId: Int64? if let replyTo = replyTo { var threadMessageId: MessageId? @@ -725,7 +729,7 @@ extension StoreMessage { threadId = Int64(threadIdValue.id) } } - attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: namespace, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) + attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote)) } if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) @@ -867,7 +871,7 @@ extension StoreMessage { attributes.append(InlineBusinessBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBusinessBotId)), title: nil)) } - if namespace != Namespaces.Message.ScheduledCloud && namespace != Namespaces.Message.QuickReplyCloud { + if !Namespaces.Message.allNonRegular.contains(namespace) { if let views = views { attributes.append(ViewCountMessageAttribute(count: Int(views))) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift index c0f8c1c109..fa1f8033ce 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingPeerMediaUploadManager.swift @@ -258,7 +258,8 @@ private final class PendingPeerMediaUploadManagerImpl { message: message, cacheReferenceKey: nil, result: result, - accountPeerId: accountPeerId + accountPeerId: accountPeerId, + pendingMessageEvent: { _ in } ) |> deliverOn(queue)).start(completed: { [weak self, weak context] in guard let strongSelf = self, let initialContext = context else { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index ac6cdb6fc3..9503219ab2 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -529,7 +529,7 @@ func initialStateWithPeerIds(_ transaction: Transaction, peerIds: Set, a } } - let state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedReplyMessageIds: referencedReplyMessageIds, initialReferencedGeneralMessageIds: referencedGeneralMessageIds, initialStoredMessages: storedMessages, initialStoredStories: storedStories, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp) + let state = AccountMutableState(initialState: AccountInitialState(state: (transaction.getState() as? AuthorizedAccountState)!.state!, peerIds: peerIds, peerIdsRequiringLocalChatState: peerIdsRequiringLocalChatState, channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: locallyGeneratedMessageTimestamps, cloudReadStates: cloudReadStates, channelsToPollExplicitely: channelsToPollExplicitely), initialPeers: peers, initialReferencedReplyMessageIds: referencedReplyMessageIds, initialReferencedGeneralMessageIds: referencedGeneralMessageIds, initialStoredMessages: storedMessages, initialStoredStories: storedStories, initialReadInboxMaxIds: readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: storedMessagesByPeerIdAndTimestamp, initialSentScheduledMessageIds: Set()) return state } @@ -1666,12 +1666,19 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: if let message = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: peerIsForum, namespace: Namespaces.Message.QuickReplyCloud) { updatedState.addQuickReplyMessages([message]) } - case let .updateDeleteScheduledMessages(peer, messages): + case let .updateDeleteScheduledMessages(_, peer, messages, sentMessages): var messageIds: [MessageId] = [] + var sentMessageIds: [MessageId] = [] for message in messages { messageIds.append(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.ScheduledCloud, id: message)) } + if let sentMessages { + for message in sentMessages { + sentMessageIds.append(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: message)) + } + } updatedState.deleteMessages(messageIds) + updatedState.addSentScheduledMessageIds(sentMessageIds) case let .updateDeleteQuickReplyMessages(_, messages): var messageIds: [MessageId] = [] for message in messages { @@ -2628,7 +2635,7 @@ func pollChannelOnce(accountPeerId: PeerId, postbox: Postbox, network: Network, peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) } } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:], initialSentScheduledMessageIds: Set()) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) @@ -2685,7 +2692,7 @@ public func standalonePollChannelOnce(accountPeerId: PeerId, postbox: Postbox, n peerChatInfos[peerId] = PeerChatInfo(notificationSettings: notificationSettings) } } - let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:]) + let initialState = AccountMutableState(initialState: AccountInitialState(state: accountState, peerIds: Set(), peerIdsRequiringLocalChatState: Set(), channelStates: channelStates, peerChatInfos: peerChatInfos, locallyGeneratedMessageTimestamps: [:], cloudReadStates: [:], channelsToPollExplicitely: Set()), initialPeers: initialPeers, initialReferencedReplyMessageIds: ReferencedReplyMessageIds(), initialReferencedGeneralMessageIds: Set(), initialStoredMessages: Set(), initialStoredStories: [:], initialReadInboxMaxIds: [:], storedMessagesByPeerIdAndTimestamp: [:], initialSentScheduledMessageIds: Set()) return pollChannel(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer, state: initialState) |> mapToSignal { (finalState, _, timeout) -> Signal in return resolveAssociatedMessages(accountPeerId: accountPeerId, postbox: postbox, network: network, state: finalState) @@ -3286,6 +3293,8 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation]) result.append(.AddQuickReplyMessages(currentAddQuickReplyMessages.messages)) } currentAddMessages = nil + currentAddScheduledMessages = nil + currentAddQuickReplyMessages = nil result.append(operation) case let .UpdateState(state): updatedState = state @@ -5340,5 +5349,29 @@ func replayFinalState( _internal_setStarsReactionDefaultToPrivate(isPrivate: updatedStarsReactionsAreAnonymousByDefault, transaction: transaction) } - return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated, updatedRevenueBalances: updatedRevenueBalances, updatedStarsBalance: updatedStarsBalance, updatedStarsRevenueStatus: updatedStarsRevenueStatus) + return AccountReplayedFinalState( + state: finalState, + addedIncomingMessageIds: addedIncomingMessageIds, + addedReactionEvents: addedReactionEvents, + wasScheduledMessageIds: wasScheduledMessageIds, + addedSecretMessageIds: addedSecretMessageIds, + deletedMessageIds: deletedMessageIds, + updatedTypingActivities: updatedTypingActivities, + updatedWebpages: updatedWebpages, + updatedCalls: updatedCalls, + addedCallSignalingData: addedCallSignalingData, + updatedGroupCallParticipants: updatedGroupCallParticipants, + storyUpdates: storyUpdates, + updatedPeersNearby: updatedPeersNearby, + isContactUpdates: isContactUpdates, + delayNotificatonsUntil: delayNotificatonsUntil, + updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, + updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, + updateConfig: updateConfig, + isPremiumUpdated: isPremiumUpdated, + updatedRevenueBalances: updatedRevenueBalances, + updatedStarsBalance: updatedStarsBalance, + updatedStarsRevenueStatus: updatedStarsRevenueStatus, + sentScheduledMessageIds: finalState.state.sentScheduledMessageIds + ) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 1ae6baf6ea..1fc793db67 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -63,6 +63,7 @@ public enum DeletedMessageId: Hashable { final class MessagesRemovedContext { private var messagesRemovedInteractively = Set() + private var messagesRemovedRemotely = Set() private var messagesRemovedInteractivelyLock = NSLock() func synchronouslyIsMessageDeletedInteractively(ids: [MessageId]) -> [EngineMessage.Id] { @@ -85,6 +86,26 @@ final class MessagesRemovedContext { return result } + func synchronouslyIsMessageDeletedRemotely(ids: [MessageId]) -> [EngineMessage.Id] { + var result: [EngineMessage.Id] = [] + + self.messagesRemovedInteractivelyLock.lock() + for id in ids { + let mappedId: DeletedMessageId + if id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup { + mappedId = .global(id.id) + } else { + mappedId = .messageId(id) + } + if self.messagesRemovedRemotely.contains(mappedId) { + result.append(id) + } + } + self.messagesRemovedInteractivelyLock.unlock() + + return result + } + func addIsMessagesDeletedInteractively(ids: [DeletedMessageId]) { if ids.isEmpty { return @@ -94,6 +115,16 @@ final class MessagesRemovedContext { self.messagesRemovedInteractively.formUnion(ids) self.messagesRemovedInteractivelyLock.unlock() } + + func addIsMessagesDeletedRemotely(ids: [DeletedMessageId]) { + if ids.isEmpty { + return + } + + self.messagesRemovedInteractivelyLock.lock() + self.messagesRemovedRemotely.formUnion(ids) + self.messagesRemovedInteractivelyLock.unlock() + } } public final class AccountStateManager { @@ -297,6 +328,11 @@ public final class AccountStateManager { return self.forceSendPendingStarsReactionPipe.signal() } + fileprivate let sentScheduledMessageIdsPipe = ValuePipe>() + public var sentScheduledMessageIds: Signal, NoError> { + return self.sentScheduledMessageIdsPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedRevenueBalancesContext = UpdatedRevenueBalancesSubscriberContext() @@ -690,6 +726,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } return result @@ -768,11 +805,16 @@ public final class AccountStateManager { |> deliverOn(self.queue) |> mapToSignal { [weak self] state, invalidatedChannels, disableParallelChannelReset -> Signal<(difference: Api.updates.Difference?, finalStatte: AccountReplayedFinalState?, skipBecauseOfError: Bool, resetState: Bool), NoError> in if let state = state, let authorizedState = state.state { - let flags: Int32 - let ptsTotalLimit: Int32? + var flags: Int32 = 0 + var ptsTotalLimit: Int32? - flags = 1 << 0 - ptsTotalLimit = 1000 + if !"".isEmpty { + flags |= 1 << 0 + ptsTotalLimit = 1000 + } + + flags = 0 + ptsTotalLimit = nil if let strongSelf = self { if !invalidatedChannels.isEmpty { @@ -830,6 +872,7 @@ public final class AccountStateManager { if let replayedState = replayedState { if !replayedState.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: replayedState.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: replayedState.deletedMessageIds) } return (difference, replayedState, false, false) @@ -968,6 +1011,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1075,6 +1119,9 @@ public final class AccountStateManager { if !events.updatedIncomingThreadReadStates.isEmpty || !events.updatedOutgoingThreadReadStates.isEmpty { strongSelf.threadReadStateUpdatesPipe.putNext((events.updatedIncomingThreadReadStates, events.updatedOutgoingThreadReadStates)) } + if !events.sentScheduledMessageIds.isEmpty { + strongSelf.sentScheduledMessageIdsPipe.putNext(events.sentScheduledMessageIds) + } if !events.isContactUpdates.isEmpty { strongSelf.addIsContactUpdates(events.isContactUpdates) } @@ -1243,6 +1290,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } return result @@ -1291,6 +1339,7 @@ public final class AccountStateManager { if let result = result, !result.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: result.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: result.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1383,6 +1432,7 @@ public final class AccountStateManager { if let replayedState = replayedState, !replayedState.deletedMessageIds.isEmpty { messagesRemovedContext.addIsMessagesDeletedInteractively(ids: replayedState.deletedMessageIds) + messagesRemovedContext.addIsMessagesDeletedRemotely(ids: replayedState.deletedMessageIds) } let deltaTime = CFAbsoluteTimeGetCurrent() - startTime @@ -1884,6 +1934,12 @@ public final class AccountStateManager { } } + public var sentScheduledMessageIds: Signal, NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.sentScheduledMessageIds.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + func forceSendPendingStarsReaction(messageId: MessageId) { self.impl.with { impl in impl.forceSendPendingStarsReactionPipe.putNext(messageId) @@ -2123,6 +2179,10 @@ public final class AccountStateManager { public func synchronouslyIsMessageDeletedInteractively(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { return self.messagesRemovedContext.synchronouslyIsMessageDeletedInteractively(ids: ids) } + + public func synchronouslyIsMessageDeletedRemotely(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { + return self.messagesRemovedContext.synchronouslyIsMessageDeletedRemotely(ids: ids) + } } func resolveNotificationSettings(list: [TelegramPeerNotificationSettings], defaultSettings: MessageNotificationSettings) -> (sound: PeerMessageSound, notify: Bool, displayContents: Bool) { diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index be44796999..03302828e0 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -58,7 +58,7 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: } } -func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, cacheReferenceKey: CachedSentMediaReferenceKey?, result: Api.Updates, accountPeerId: PeerId) -> Signal { +func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, cacheReferenceKey: CachedSentMediaReferenceKey?, result: Api.Updates, accountPeerId: PeerId, pendingMessageEvent: @escaping (PeerPendingMessageDelivered) -> Void) -> Signal { return postbox.transaction { transaction -> Void in let messageId: Int32? var apiMessage: Api.Message? @@ -125,7 +125,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var sentStickers: [TelegramMediaFile] = [] var sentGifs: [TelegramMediaFile] = [] - if let updatedTimestamp = updatedTimestamp { + if let updatedTimestamp { transaction.offsetPendingMessagesTimestamps(lowerBound: message.id, excludeIds: Set([message.id]), timestamp: updatedTimestamp) } @@ -134,29 +134,18 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] transaction.updateMessage(message.id, update: { currentMessage in - let updatedId: MessageId - if let messageId = messageId { - var namespace: MessageId.Namespace = Namespaces.Message.Cloud - if Namespaces.Message.allQuickReply.contains(message.id.namespace) { - namespace = Namespaces.Message.QuickReplyCloud - } else if let updatedTimestamp = updatedTimestamp { - if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp { - namespace = Namespaces.Message.ScheduledCloud - } - } else if Namespaces.Message.allScheduled.contains(message.id.namespace) { - namespace = Namespaces.Message.ScheduledCloud - } - updatedId = MessageId(peerId: currentMessage.id.peerId, namespace: namespace, id: messageId) - } else { - updatedId = currentMessage.id - } - let media: [Media] var attributes: [MessageAttribute] let text: String let forwardInfo: StoreMessageForwardInfo? let threadId: Int64? - if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForum ?? false) { + + var namespace = Namespaces.Message.Cloud + if message.id.namespace == Namespaces.Message.ScheduledLocal { + namespace = Namespaces.Message.ScheduledCloud + } + + if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForum ?? false, namespace: namespace) { media = updatedMessage.media attributes = updatedMessage.attributes text = updatedMessage.text @@ -195,12 +184,10 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes updatedAttributes.append(MediaSpoilerMessageAttribute()) } - if Namespaces.Message.allScheduled.contains(message.id.namespace) && updatedId.namespace == Namespaces.Message.Cloud { - for i in 0 ..< updatedAttributes.count { - if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute { - updatedAttributes.remove(at: i) - break - } + for i in 0 ..< updatedAttributes.count { + if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute { + updatedAttributes.remove(at: i) + break } } if Namespaces.Message.allQuickReply.contains(message.id.namespace) { @@ -225,6 +212,30 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes threadId = currentMessage.threadId } + let updatedId: MessageId + if let messageId = messageId { + var namespace: MessageId.Namespace = Namespaces.Message.Cloud + if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) { + namespace = Namespaces.Message.ScheduledCloud + } + if Namespaces.Message.allQuickReply.contains(message.id.namespace) { + namespace = Namespaces.Message.QuickReplyCloud + } else if let updatedTimestamp = updatedTimestamp { + if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) { + namespace = Namespaces.Message.ScheduledCloud + } else { + if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp { + namespace = Namespaces.Message.ScheduledCloud + } + } + } else if Namespaces.Message.allScheduled.contains(message.id.namespace) { + namespace = Namespaces.Message.ScheduledCloud + } + updatedId = MessageId(peerId: currentMessage.id.peerId, namespace: namespace, id: messageId) + } else { + updatedId = currentMessage.id + } + for attribute in currentMessage.attributes { if let attribute = attribute as? OutgoingMessageInfoAttribute { bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets @@ -358,12 +369,26 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes stateManager.addUpdates(result) stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: message.id.peerId)]) + + if let updatedMessage, case let .Id(id) = updatedMessage.id { + pendingMessageEvent(PeerPendingMessageDelivered( + id: id, + isSilent: updatedMessage.attributes.contains(where: { attribute in + if let attribute = attribute as? NotificationInfoMessageAttribute { + return attribute.flags.contains(.muted) + } else { + return false + } + }), + isPendingProcessing: updatedMessage.attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) + )) + } } } -func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal { +func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates, pendingMessageEvents: @escaping ([PeerPendingMessageDelivered]) -> Void) -> Signal { guard !messages.isEmpty else { - return .complete() + return .single(Void()) } return postbox.transaction { transaction -> Void in @@ -372,8 +397,12 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage var namespace = Namespaces.Message.Cloud if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) { namespace = Namespaces.Message.QuickReplyCloud - } else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { - namespace = Namespaces.Message.ScheduledCloud + } else if let message = messages.first, let apiMessage = result.messages.first { + if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + namespace = Namespaces.Message.ScheduledCloud + } else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 { + namespace = Namespaces.Message.ScheduledCloud + } } var resultMessages: [MessageId: StoreMessage] = [:] @@ -538,6 +567,23 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage } stateManager.addUpdates(result) stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: messages[0].id.peerId)]) + + pendingMessageEvents(mapping.compactMap { message, _, updatedMessage -> PeerPendingMessageDelivered? in + guard case let .Id(id) = updatedMessage.id else { + return nil + } + return PeerPendingMessageDelivered( + id: id, + isSilent: updatedMessage.attributes.contains(where: { attribute in + if let attribute = attribute as? NotificationInfoMessageAttribute { + return attribute.flags.contains(.muted) + } else { + return false + } + }), + isPendingProcessing: updatedMessage.attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) + ) + }) } } diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index 82e652ade4..743c030556 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -223,7 +223,6 @@ public final class AvailableReactions: Equatable, Codable { var reactions = reactions reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } }) - //TODO:release reactions.append(generateStarsReaction()) self.reactions = reactions } @@ -243,7 +242,6 @@ public final class AvailableReactions: Equatable, Codable { self.hash = try container.decodeIfPresent(Int32.self, forKey: .newHash) ?? 0 - //TODO:release var reactions = try container.decode([Reaction].self, forKey: .reactions) reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } }) reactions.append(generateStarsReaction()) diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index ff6734e87f..eec637f81f 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -98,8 +98,20 @@ func sendMessageReasonForError(_ error: String) -> PendingMessageFailureReason? } } +public struct PeerPendingMessageDelivered { + public var id: EngineMessage.Id + public var isSilent: Bool + public var isPendingProcessing: Bool + + public init(id: EngineMessage.Id, isSilent: Bool, isPendingProcessing: Bool) { + self.id = id + self.isSilent = isSilent + self.isPendingProcessing = isPendingProcessing + } +} + private final class PeerPendingMessagesSummaryContext { - var messageDeliveredSubscribers = Bag<((MessageId.Namespace, Bool)) -> Void>() + var messageDeliveredSubscribers = Bag<([PeerPendingMessageDelivered]) -> Void>() var messageFailedSubscribers = Bag<(PendingMessageFailureReason) -> Void>() } @@ -270,29 +282,32 @@ public final class PendingMessageManager { } if !removedSecretMessageIds.isEmpty { - let _ = (self.postbox.transaction { transaction -> (Set, Bool) in - var silent = false - var peerIdsWithDeliveredMessages = Set() + let _ = (self.postbox.transaction { transaction -> [PeerId: [PeerPendingMessageDelivered]] in + var peerIdsWithDeliveredMessages: [PeerId: [PeerPendingMessageDelivered]] = [:] for id in removedSecretMessageIds { if let message = transaction.getMessage(id) { if message.isSentOrAcknowledged { - peerIdsWithDeliveredMessages.insert(id.peerId) + var silent = false if message.muted { silent = true } + if peerIdsWithDeliveredMessages[id.peerId] == nil { + peerIdsWithDeliveredMessages[id.peerId] = [] + } + peerIdsWithDeliveredMessages[id.peerId]?.append(PeerPendingMessageDelivered(id: MessageId(peerId: id.peerId, namespace: Namespaces.Message.Cloud, id: id.id), isSilent: silent, isPendingProcessing: false)) } } } - return (peerIdsWithDeliveredMessages, silent) + return peerIdsWithDeliveredMessages } - |> deliverOn(self.queue)).start(next: { [weak self] peerIdsWithDeliveredMessages, silent in + |> deliverOn(self.queue)).start(next: { [weak self] peerIdsWithDeliveredMessages in guard let strongSelf = self else { return } - for peerId in peerIdsWithDeliveredMessages { + for (peerId, deliveredMessages) in peerIdsWithDeliveredMessages { if let context = strongSelf.peerSummaryContexts[peerId] { for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber((Namespaces.Message.Cloud, silent)) + subscriber(deliveredMessages) } } } @@ -1724,43 +1739,49 @@ public final class PendingMessageManager { } } - let silent = message.muted - var namespace = Namespaces.Message.Cloud if message.id.namespace == Namespaces.Message.QuickReplyLocal { - namespace = Namespaces.Message.QuickReplyCloud - } else if let apiMessage = apiMessage, let id = apiMessage.id(namespace: message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { - namespace = id.namespace - - if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { - self.correlationIdToSentMessageId.with { value in - value.mapping[correlationId] = id + } else if let apiMessage { + var isScheduled = false + if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + isScheduled = true + } + if case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage { + if (flags2 & (1 << 4)) != 0 { + isScheduled = true } } - } - - return applyUpdateMessage(postbox: postbox, stateManager: stateManager, message: message, cacheReferenceKey: content.cacheReferenceKey, result: result, accountPeerId: self.accountPeerId) - |> afterDisposed { [weak self] in - if let strongSelf = self { - strongSelf.queue.async { - if let context = strongSelf.peerSummaryContexts[message.id.peerId] { - for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber((namespace, silent)) - } + if let id = apiMessage.id(namespace: isScheduled ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud) { + if let attribute = message.attributes.first(where: { $0 is OutgoingMessageInfoAttribute }) as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId { + self.correlationIdToSentMessageId.with { value in + value.mapping[correlationId] = id } } } } + + let queue = self.queue + return applyUpdateMessage(postbox: postbox, stateManager: stateManager, message: message, cacheReferenceKey: content.cacheReferenceKey, result: result, accountPeerId: self.accountPeerId, pendingMessageEvent: { [weak self] pendingMessageDelivered in + queue.async { + if let strongSelf = self { + if let context = strongSelf.peerSummaryContexts[message.id.peerId] { + for subscriber in context.messageDeliveredSubscribers.copyItems() { + subscriber([pendingMessageDelivered]) + } + } + } + } + }) } private func applySentGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates) -> Signal { - var silent = false var namespace = Namespaces.Message.Cloud - if let message = messages.first, message.id.namespace == Namespaces.Message.QuickReplyLocal { - namespace = Namespaces.Message.QuickReplyCloud - } else if let message = messages.first, let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { - namespace = Namespaces.Message.ScheduledCloud - if message.muted { - silent = true + if let message = messages.first { + if message.id.namespace == Namespaces.Message.QuickReplyLocal { + namespace = Namespaces.Message.QuickReplyCloud + } else if let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp { + namespace = Namespaces.Message.ScheduledCloud + } else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 { + namespace = Namespaces.Message.ScheduledCloud } } @@ -1777,22 +1798,22 @@ public final class PendingMessageManager { } } } + let queue = self.queue - return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result) - |> afterDisposed { [weak self] in - if let strongSelf = self { - strongSelf.queue.async { - if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId] { + return applyUpdateGroupMessages(postbox: postbox, stateManager: stateManager, messages: messages, result: result, pendingMessageEvents: { [weak self] pendingMessagesDelivered in + queue.async { + if let strongSelf = self { + if let message = messages.first, let context = strongSelf.peerSummaryContexts[message.id.peerId], !pendingMessagesDelivered.isEmpty { for subscriber in context.messageDeliveredSubscribers.copyItems() { - subscriber((namespace, silent)) + subscriber(pendingMessagesDelivered) } } } } - } + }) } - public func deliveredMessageEvents(peerId: PeerId) -> Signal<(namespace: MessageId.Namespace, silent: Bool), NoError> { + public func deliveredMessageEvents(peerId: PeerId) -> Signal<[PeerPendingMessageDelivered], NoError> { return Signal { subscriber in let disposable = MetaDisposable() @@ -1805,8 +1826,8 @@ public final class PendingMessageManager { self.peerSummaryContexts[peerId] = summaryContext } - let index = summaryContext.messageDeliveredSubscribers.add({ namespace, silent in - subscriber.putNext((namespace, silent)) + let index = summaryContext.messageDeliveredSubscribers.add({ event in + subscriber.putNext(event) }) disposable.set(ActionDisposable { diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 901664c8fd..3450b1b4a7 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 190 + return 192 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift b/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift index bed0706667..b61049a1f4 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizePeerReadState.swift @@ -178,7 +178,9 @@ private func validatePeerReadState(network: Network, postbox: Postbox, stateMana if case let .idBased(updatedMaxIncomingReadId, _, _, updatedCount, updatedMarkedUnread) = readState { if updatedCount != 0 || updatedMarkedUnread { if localMaxIncomingReadId > updatedMaxIncomingReadId { - return .retry + if !"".isEmpty { + return .retry + } } } } diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index 79f03b8308..f667991512 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -115,7 +115,11 @@ extension Api.Message { func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? { switch self { - case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .message(_, flags2, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + var namespace = namespace + if (flags2 & (1 << 4)) != 0 { + namespace = Namespaces.Message.ScheduledCloud + } let peerId: PeerId = messagePeerId.peerId return MessageId(peerId: peerId, namespace: namespace, id: id) case let .messageEmpty(_, id, peerId): diff --git a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift index 90ad669f1c..7fafc906bc 100644 --- a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift @@ -145,7 +145,7 @@ private func requestRevenueStats(postbox: Postbox, network: Network, peerId: Pee } return nil } |> mapToSignal { peer -> Signal in - guard let peer, let inputChannel = apiInputChannel(peer) else { + guard let peer, let inputPeer = apiInputPeer(peer) else { return .never() } @@ -154,7 +154,7 @@ private func requestRevenueStats(postbox: Postbox, network: Network, peerId: Pee flags |= (1 << 1) } - return network.request(Api.functions.stats.getBroadcastRevenueStats(flags: flags, channel: inputChannel)) + return network.request(Api.functions.stats.getBroadcastRevenueStats(flags: flags, peer: inputPeer)) |> map { result -> RevenueStats? in return RevenueStats(apiRevenueStats: result, peerId: peerId) } @@ -354,13 +354,13 @@ private final class RevenueStatsTransactionsContextImpl { } |> mapToSignal { peer -> Signal<([RevenueStatsTransactionsContext.State.Transaction], Int32, Int32?), NoError> in if let peer { - guard let inputChannel = apiInputChannel(peer) else { + guard let inputPeer = apiInputPeer(peer) else { return .complete() } let offset = lastOffset ?? 0 let limit: Int32 = lastOffset == nil ? 25 : 50 - return account.network.request(Api.functions.stats.getBroadcastRevenueTransactions(channel: inputChannel, offset: offset, limit: limit), automaticFloodWait: false) + return account.network.request(Api.functions.stats.getBroadcastRevenueTransactions(peer: inputPeer, offset: offset, limit: limit), automaticFloodWait: false) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -502,7 +502,7 @@ public enum RequestRevenueWithdrawalError : Equatable { } func _internal_checkChannelRevenueWithdrawalAvailability(account: Account) -> Signal { - return account.network.request(Api.functions.stats.getBroadcastRevenueWithdrawalUrl(channel: .inputChannelEmpty, password: .inputCheckPasswordEmpty)) + return account.network.request(Api.functions.stats.getBroadcastRevenueWithdrawalUrl(peer: .inputPeerEmpty, password: .inputCheckPasswordEmpty)) |> mapError { error -> RequestRevenueWithdrawalError in if error.errorDescription == "PASSWORD_HASH_INVALID" { return .requestPassword @@ -530,7 +530,7 @@ func _internal_requestChannelRevenueWithdrawalUrl(account: Account, peerId: Peer } return account.postbox.transaction { transaction -> Signal in - guard let channel = transaction.getPeer(peerId) as? TelegramChannel, let inputChannel = apiInputChannel(channel) else { + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } @@ -555,7 +555,7 @@ func _internal_requestChannelRevenueWithdrawalUrl(account: Account, peerId: Peer return checkPassword |> mapToSignal { password -> Signal in - return account.network.request(Api.functions.stats.getBroadcastRevenueWithdrawalUrl(channel: inputChannel, password: password), automaticFloodWait: false) + return account.network.request(Api.functions.stats.getBroadcastRevenueWithdrawalUrl(peer: inputPeer, password: password), automaticFloodWait: false) |> mapError { error -> RequestRevenueWithdrawalError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 0cf834f0db..3e5616334f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -298,6 +298,7 @@ public struct CachedUserFlags: OptionSet { public static let readDatesPrivate = CachedUserFlags(rawValue: 1 << 2) public static let premiumRequired = CachedUserFlags(rawValue: 1 << 3) public static let adsEnabled = CachedUserFlags(rawValue: 1 << 4) + public static let canViewRevenue = CachedUserFlags(rawValue: 1 << 5) } public final class EditableBotInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_WasScheduledMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_WasScheduledMessageAttribute.swift index 00696fa9fd..56bd059241 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_WasScheduledMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_WasScheduledMessageAttribute.swift @@ -1,13 +1,18 @@ import Foundation import Postbox -public class WasScheduledMessageAttribute: MessageAttribute { - public init() { +public class PendingProcessingMessageAttribute: MessageAttribute { + public let approximateCompletionTime: Int32 + + public init(approximateCompletionTime: Int32) { + self.approximateCompletionTime = approximateCompletionTime } required public init(decoder: PostboxDecoder) { + self.approximateCompletionTime = decoder.decodeInt32ForKey("et", orElse: 0) } public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.approximateCompletionTime, forKey: "et") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index d36eeccb24..f8ef324e33 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1116,7 +1116,9 @@ public extension TelegramEngine.EngineData.Item { guard let view = view as? CachedPeerDataView else { preconditionFailure() } - if let cachedData = view.cachedPeerData as? CachedChannelData { + if let cachedData = view.cachedPeerData as? CachedUserData { + return cachedData.flags.contains(.canViewRevenue) + } else if let cachedData = view.cachedPeerData as? CachedChannelData { return cachedData.flags.contains(.canViewRevenue) } else { return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index ee9351f768..20c87296ea 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -420,14 +420,14 @@ private class AdMessagesHistoryContextImpl { } }) - let signal: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputChannel? in - return transaction.getPeer(peerId).flatMap(apiInputChannel) + let signal: Signal<(interPostInterval: Int32?, messages: [Message]), NoError> = account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputChannel -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in - guard let inputChannel = inputChannel else { + |> mapToSignal { inputPeer -> Signal<(interPostInterval: Int32?, messages: [Message]), NoError> in + guard let inputPeer else { return .single((nil, [])) } - return account.network.request(Api.functions.channels.getSponsoredMessages(channel: inputChannel)) + return account.network.request(Api.functions.messages.getSponsoredMessages(peer: inputPeer)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -515,14 +515,14 @@ private class AdMessagesHistoryContextImpl { } func markAsSeen(opaqueId: Data) { - let signal: Signal = account.postbox.transaction { transaction -> Api.InputChannel? in - return transaction.getPeer(self.peerId).flatMap(apiInputChannel) + let signal: Signal = account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(self.peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputChannel -> Signal in - guard let inputChannel = inputChannel else { + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { return .complete() } - return self.account.network.request(Api.functions.channels.viewSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId))) + return self.account.network.request(Api.functions.messages.viewSponsoredMessage(peer: inputPeer, randomId: Buffer(data: opaqueId))) |> `catch` { _ -> Signal in return .single(.boolFalse) } @@ -608,11 +608,11 @@ public class AdMessagesHistoryContext { func _internal_markAdAction(account: Account, peerId: EnginePeer.Id, opaqueId: Data, media: Bool, fullscreen: Bool) { - let signal: Signal = account.postbox.transaction { transaction -> Api.InputChannel? in - return transaction.getPeer(peerId).flatMap(apiInputChannel) + let signal: Signal = account.postbox.transaction { transaction -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) } - |> mapToSignal { inputChannel -> Signal in - guard let inputChannel = inputChannel else { + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { return .complete() } var flags: Int32 = 0 @@ -622,7 +622,7 @@ func _internal_markAdAction(account: Account, peerId: EnginePeer.Id, opaqueId: D if fullscreen { flags |= (1 << 1) } - return account.network.request(Api.functions.channels.clickSponsoredMessage(flags: flags, channel: inputChannel, randomId: Buffer(data: opaqueId))) + return account.network.request(Api.functions.messages.clickSponsoredMessage(flags: flags, peer: inputPeer, randomId: Buffer(data: opaqueId))) |> `catch` { _ -> Signal in return .single(.boolFalse) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 622ddeac15..dac0e5b6db 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -10,9 +10,9 @@ private let botWebViewPlatform = "macos" private let botWebViewPlatform = "ios" #endif -public enum RequestSimpleWebViewSource { +public enum RequestSimpleWebViewSource : Equatable { case generic - case inline + case inline(startParam: String?) case settings } @@ -30,8 +30,12 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P if let _ = serializedThemeParams { flags |= (1 << 0) } + + var startParam: String? = nil + switch source { - case .inline: + case let .inline(_startParam): + startParam = _startParam flags |= (1 << 1) case .settings: flags |= (1 << 2) @@ -41,7 +45,7 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P if let _ = url { flags |= (1 << 3) } - return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) + return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: startParam, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestWebViewError in return .generic } @@ -77,15 +81,18 @@ func _internal_requestMainWebView(postbox: Postbox, network: Network, botId: Pee if let _ = serializedThemeParams { flags |= (1 << 0) } + var startParam: String? = nil + switch source { - case .inline: + case let .inline(_startParam): + startParam = _startParam flags |= (1 << 1) case .settings: flags |= (1 << 2) default: break } - return network.request(Api.functions.messages.requestMainWebView(flags: flags, peer: inputPeer, bot: inputUser, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) + return network.request(Api.functions.messages.requestMainWebView(flags: flags, peer: inputPeer, bot: inputUser, startParam: startParam, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestWebViewError in return .generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportAds.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportAds.swift index ae07cda8da..61fb824b75 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportAds.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportAds.swift @@ -21,10 +21,10 @@ public enum ReportAdMessageError { func _internal_reportAdMessage(account: Account, peerId: EnginePeer.Id, opaqueId: Data, option: Data?) -> Signal { return account.postbox.transaction { transaction -> Signal in - guard let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) else { + guard let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } - return account.network.request(Api.functions.channels.reportSponsoredMessage(channel: inputChannel, randomId: Buffer(data: opaqueId), option: Buffer(data: option))) + return account.network.request(Api.functions.messages.reportSponsoredMessage(peer: inputPeer, randomId: Buffer(data: opaqueId), option: Buffer(data: option))) |> mapError { error -> ReportAdMessageError in if error.errorDescription == "PREMIUM_ACCOUNT_REQUIRED" { return .premiumRequired diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 77b650f91e..b6e2c1fd7f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1297,7 +1297,7 @@ public final class PeerStoryListContext: StoryListContext { public final class SearchStoryListContext: StoryListContext { public enum Source { - case hashtag(String) + case hashtag(EnginePeer.Id?, String) case mediaArea(MediaArea) } @@ -1368,21 +1368,33 @@ public final class SearchStoryListContext: StoryListContext { var searchHashtag: String? = nil var area: Api.MediaArea? = nil + var peer: Signal = .single(nil) var flags: Int32 = 0 switch source { - case let .hashtag(query): + case let .hashtag(peerId, query): if query.hasPrefix("#") { searchHashtag = String(query[query.index(after: query.startIndex)...]) } else { searchHashtag = query } flags |= (1 << 0) + + if let peerId { + peer = account.postbox.transaction { transaction in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + flags |= (1 << 2) + } case let .mediaArea(mediaArea): area = apiMediaAreasFromMediaAreas([mediaArea], transaction: nil).first flags |= (1 << 1) } - self.requestDisposable = (account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, area: area, offset: loadMoreToken, limit: Int32(limit))) + self.requestDisposable = (peer + |> castError(MTRpcError.self) + |> mapToSignal { inputPeer in + return account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, area: area, peer: inputPeer, offset: loadMoreToken, limit: Int32(limit))) + } |> map { result -> Api.stories.FoundStories? in return result } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 5933679e31..202922f2ab 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1401,6 +1401,10 @@ public extension TelegramEngine { return self.account.stateManager.synchronouslyIsMessageDeletedInteractively(ids: ids) } + public func synchronouslyIsMessageDeletedRemotely(ids: [EngineMessage.Id]) -> [EngineMessage.Id] { + return self.account.stateManager.synchronouslyIsMessageDeletedRemotely(ids: ids) + } + public func synchronouslyLookupCorrelationId(correlationId: Int64) -> EngineMessage.Id? { return self.account.pendingMessageManager.synchronouslyLookupCorrelationId(correlationId: correlationId) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index e6592d112d..3fed134536 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -8,7 +8,7 @@ public enum BotPaymentInvoiceSource { case message(MessageId) case slug(String) case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption) - case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption) + case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption, text: String?, entities: [MessageTextEntity]?) case stars(option: StarsTopUpOption) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) case starsChatSubscription(hash: String) @@ -283,7 +283,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) return .inputInvoicePremiumGiftCode(purpose: inputPurpose, option: option) - case let .giftCode(users, currency, amount, option): + case let .giftCode(users, currency, amount, option, text, entities): var inputUsers: [Api.InputUser] = [] if !users.isEmpty { for peerId in users { @@ -293,7 +293,14 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv } } - let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: nil) + var inputPurposeFlags: Int32 = 0 + var textWithEntities: Api.TextWithEntities? + if let text, let entities { + inputPurposeFlags |= (1 << 1) + textWithEntities = .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary())) + } + + let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: inputPurposeFlags, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: textWithEntities) var flags: Int32 = 0 if let _ = option.storeProductId { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 1aaffb2ea5..85a0117c97 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -34,6 +34,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { case price case convertStars case availability + case soldOut } public struct Availability: Equatable, Codable, PostboxCoding { @@ -45,7 +46,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { public let remains: Int32 public let total: Int32 - init(remains: Int32, total: Int32) { + public init(remains: Int32, total: Int32) { self.remains = remains self.total = total } @@ -61,6 +62,31 @@ public struct StarGift: Equatable, Codable, PostboxCoding { } } + public struct SoldOut: Equatable, Codable, PostboxCoding { + enum CodingKeys: String, CodingKey { + case firstSale + case lastSale + } + + public let firstSale: Int32 + public let lastSale: Int32 + + public init(firstSale: Int32, lastSale: Int32) { + self.firstSale = firstSale + self.lastSale = lastSale + } + + public init(decoder: PostboxDecoder) { + self.firstSale = decoder.decodeInt32ForKey(CodingKeys.firstSale.rawValue, orElse: 0) + self.lastSale = decoder.decodeInt32ForKey(CodingKeys.lastSale.rawValue, orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.firstSale, forKey: CodingKeys.firstSale.rawValue) + encoder.encodeInt32(self.lastSale, forKey: CodingKeys.lastSale.rawValue) + } + } + public enum DecodingError: Error { case generic } @@ -70,13 +96,15 @@ public struct StarGift: Equatable, Codable, PostboxCoding { public let price: Int64 public let convertStars: Int64 public let availability: Availability? + public let soldOut: SoldOut? - public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?) { + public init(id: Int64, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?) { self.id = id self.file = file self.price = price self.convertStars = convertStars self.availability = availability + self.soldOut = soldOut } public init(from decoder: Decoder) throws { @@ -92,6 +120,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { self.price = try container.decode(Int64.self, forKey: .price) self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars) ?? 0 self.availability = try container.decodeIfPresent(Availability.self, forKey: .availability) + self.soldOut = try container.decodeIfPresent(SoldOut.self, forKey: .soldOut) } public init(decoder: PostboxDecoder) { @@ -100,6 +129,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { self.price = decoder.decodeInt64ForKey(CodingKeys.price.rawValue, orElse: 0) self.convertStars = decoder.decodeInt64ForKey(CodingKeys.convertStars.rawValue, orElse: 0) self.availability = decoder.decodeObjectForKey(CodingKeys.availability.rawValue, decoder: { StarGift.Availability(decoder: $0) }) as? StarGift.Availability + self.soldOut = decoder.decodeObjectForKey(CodingKeys.soldOut.rawValue, decoder: { StarGift.SoldOut(decoder: $0) }) as? StarGift.SoldOut } public func encode(to encoder: Encoder) throws { @@ -114,6 +144,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { try container.encode(self.price, forKey: .price) try container.encode(self.convertStars, forKey: .convertStars) try container.encodeIfPresent(self.availability, forKey: .availability) + try container.encodeIfPresent(self.soldOut, forKey: .soldOut) } public func encode(_ encoder: PostboxEncoder) { @@ -126,21 +157,30 @@ public struct StarGift: Equatable, Codable, PostboxCoding { } else { encoder.encodeNil(forKey: CodingKeys.availability.rawValue) } + if let soldOut = self.soldOut { + encoder.encodeObject(soldOut, forKey: CodingKeys.soldOut.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.soldOut.rawValue) + } } } extension StarGift { init?(apiStarGift: Api.StarGift) { switch apiStarGift { - case let .starGift(_, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars): + case let .starGift(_, id, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSale, lastSale): var availability: Availability? if let availabilityRemains, let availabilityTotal { availability = Availability(remains: availabilityRemains, total: availabilityTotal) } + var soldOut: SoldOut? + if let firstSale, let lastSale { + soldOut = SoldOut(firstSale: firstSale, lastSale: lastSale) + } guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else { return nil } - self.init(id: id, file: file, price: stars, convertStars: convertStars, availability: availability) + self.init(id: id, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut) } } } @@ -248,6 +288,8 @@ func _internal_updateStarGiftAddedToProfile(account: Account, messageId: EngineM } } +private var cachedAccountGifts: [EnginePeer.Id: [ProfileGiftsContext.State.StarGift]] = [:] + private final class ProfileGiftsContextImpl { private let queue: Queue private let account: Account @@ -260,6 +302,7 @@ private final class ProfileGiftsContextImpl { private var count: Int32? private var dataState: ProfileGiftsContext.State.DataState = .ready(canLoadMore: true, nextOffset: nil) + var _state: ProfileGiftsContext.State? private let stateValue = Promise() var state: Signal { return self.stateValue.get() @@ -279,7 +322,11 @@ private final class ProfileGiftsContextImpl { } func loadMore() { - if case let .ready(true, nextOffset) = self.dataState { + if case let .ready(true, initialNextOffset) = self.dataState { + if self.gifts.isEmpty, self.peerId == self.account.peerId, let cachedGifts = cachedAccountGifts[self.peerId] { + self.gifts = cachedGifts + } + self.dataState = .loading self.pushState() @@ -294,7 +341,7 @@ private final class ProfileGiftsContextImpl { guard let inputUser else { return .single(([], 0, nil)) } - return network.request(Api.functions.payments.getUserStarGifts(userId: inputUser, offset: nextOffset ?? "", limit: 32)) + return network.request(Api.functions.payments.getUserStarGifts(userId: inputUser, offset: initialNextOffset ?? "", limit: 32)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -321,8 +368,13 @@ private final class ProfileGiftsContextImpl { guard let strongSelf = self else { return } - for gift in gifts { - strongSelf.gifts.append(gift) + if initialNextOffset == nil, strongSelf.peerId == strongSelf.account.peerId { + cachedAccountGifts[strongSelf.peerId] = gifts + strongSelf.gifts = gifts + } else { + for gift in gifts { + strongSelf.gifts.append(gift) + } } let updatedCount = max(Int32(strongSelf.gifts.count), count) @@ -355,6 +407,7 @@ private final class ProfileGiftsContextImpl { } private func pushState() { + self._state = ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState) self.stateValue.set(.single(ProfileGiftsContext.State(gifts: self.gifts, count: self.count, dataState: self.dataState))) } } @@ -438,6 +491,14 @@ public final class ProfileGiftsContext { impl.convertStarGift(messageId: messageId) } } + + public var currentState: ProfileGiftsContext.State? { + var state: ProfileGiftsContext.State? + self.impl.syncWith { impl in + state = impl._state + } + return state + } } private extension ProfileGiftsContext.State.StarGift { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 3e0ed0eab8..48ea7ace5f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -468,7 +468,7 @@ private final class StarsContextImpl { } var transactions = state.transactions if addTransaction { - transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil), at: 0) + transactions.insert(.init(flags: [.isLocal], id: "\(arc4random())", count: balance, date: Int32(Date().timeIntervalSince1970), peer: .appStore, title: nil, description: nil, photo: nil, transactionDate: nil, transactionUrl: nil, paidMessageId: nil, giveawayMessageId: nil, media: [], subscriptionPeriod: nil, starGift: nil, floodskipNumber: nil), at: 0) } self.updateState(StarsContext.State(flags: [.isPendingBalance], balance: max(0, state.balance + balance), subscriptions: state.subscriptions, canLoadMoreSubscriptions: state.canLoadMoreSubscriptions, transactions: transactions, canLoadMoreTransactions: state.canLoadMoreTransactions, isLoading: state.isLoading)) @@ -490,7 +490,7 @@ private final class StarsContextImpl { private extension StarsContext.State.Transaction { init?(apiTransaction: Api.StarsTransaction, peerId: EnginePeer.Id?, transaction: Transaction) { switch apiTransaction { - case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift): + case let .starsTransaction(apiFlags, id, stars, date, transactionPeer, title, description, photo, transactionDate, transactionUrl, _, messageId, extendedMedia, subscriptionPeriod, giveawayPostId, starGift, floodskipNumber): let parsedPeer: StarsContext.State.Transaction.Peer var paidMessageId: MessageId? var giveawayMessageId: MessageId? @@ -506,6 +506,8 @@ private extension StarsContext.State.Transaction { parsedPeer = .premiumBot case .starsTransactionPeerAds: parsedPeer = .ads + case .starsTransactionPeerAPI: + parsedPeer = .apiLimitExtension case .starsTransactionPeerUnsupported: parsedPeer = .unsupported case let .starsTransactionPeer(apiPeer): @@ -544,7 +546,7 @@ private extension StarsContext.State.Transaction { let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod - self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }) + self.init(flags: flags, id: id, count: stars, date: date, peer: parsedPeer, title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), transactionDate: transactionDate, transactionUrl: transactionUrl, paidMessageId: paidMessageId, giveawayMessageId: giveawayMessageId, media: media, subscriptionPeriod: subscriptionPeriod, starGift: starGift.flatMap { StarGift(apiStarGift: $0) }, floodskipNumber: floodskipNumber) } } } @@ -595,6 +597,7 @@ public final class StarsContext { case fragment case premiumBot case ads + case apiLimitExtension case unsupported case peer(EnginePeer) } @@ -614,6 +617,7 @@ public final class StarsContext { public let media: [Media] public let subscriptionPeriod: Int32? public let starGift: StarGift? + public let floodskipNumber: Int32? public init( flags: Flags, @@ -630,7 +634,8 @@ public final class StarsContext { giveawayMessageId: MessageId?, media: [Media], subscriptionPeriod: Int32?, - starGift: StarGift? + starGift: StarGift?, + floodskipNumber: Int32? ) { self.flags = flags self.id = id @@ -647,6 +652,7 @@ public final class StarsContext { self.media = media self.subscriptionPeriod = subscriptionPeriod self.starGift = starGift + self.floodskipNumber = floodskipNumber } public static func == (lhs: Transaction, rhs: Transaction) -> Bool { @@ -695,6 +701,9 @@ public final class StarsContext { if lhs.starGift != rhs.starGift { return false } + if lhs.floodskipNumber != rhs.floodskipNumber { + return false + } return true } } @@ -869,10 +878,10 @@ public final class StarsContext { private final class StarsTransactionsContextImpl { private let account: Account private weak var starsContext: StarsContext? - private let peerId: EnginePeer.Id + fileprivate let peerId: EnginePeer.Id private let mode: StarsTransactionsContext.Mode - private var _state: StarsTransactionsContext.State + fileprivate var _state: StarsTransactionsContext.State private let _statePromise = Promise() var state: Signal { return self._statePromise.get() @@ -885,17 +894,24 @@ private final class StarsTransactionsContextImpl { init(account: Account, subject: StarsTransactionsContext.Subject, mode: StarsTransactionsContext.Mode) { assert(Queue.mainQueue().isCurrent()) + + let currentTransactions: [StarsContext.State.Transaction] + self.account = account switch subject { + case let .starsTransactionsContext(transactionsContext): + self.peerId = transactionsContext.peerId + currentTransactions = transactionsContext.currentState?.transactions ?? [] case let .starsContext(starsContext): self.starsContext = starsContext self.peerId = starsContext.peerId + currentTransactions = starsContext.currentState?.transactions ?? [] case let .peer(peerId): self.peerId = peerId + currentTransactions = [] } self.mode = mode - let currentTransactions = self.starsContext?.currentState?.transactions ?? [] let initialTransactions: [StarsContext.State.Transaction] switch mode { case .all: @@ -909,7 +925,33 @@ private final class StarsTransactionsContextImpl { self._state = StarsTransactionsContext.State(transactions: initialTransactions, canLoadMore: true, isLoading: false) self._statePromise.set(.single(self._state)) - if let starsContext = self.starsContext { + if case let .starsTransactionsContext(transactionsContext) = subject { + self.stateDisposable = (transactionsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + let currentTransactions = state.transactions + let filteredTransactions: [StarsContext.State.Transaction] + switch mode { + case .all: + filteredTransactions = currentTransactions + case .incoming: + filteredTransactions = currentTransactions.filter { $0.count > 0 } + case .outgoing: + filteredTransactions = currentTransactions.filter { $0.count < 0 } + } + + if !filteredTransactions.isEmpty && self._state.transactions.isEmpty && filteredTransactions != initialTransactions { + var updatedState = self._state + updatedState.transactions.removeAll(where: { $0.flags.contains(.isLocal) }) + for transaction in filteredTransactions.reversed() { + updatedState.transactions.insert(transaction, at: 0) + } + self.updateState(updatedState) + } + }) + } else if case let .starsContext(starsContext) = subject { self.stateDisposable = (starsContext.state |> deliverOnMainQueue).start(next: { [weak self] state in guard let self, let state else { @@ -1012,6 +1054,7 @@ public final class StarsTransactionsContext { fileprivate let impl: QueueLocalObject public enum Subject { + case starsTransactionsContext(StarsTransactionsContext) case starsContext(StarsContext) case peer(EnginePeer.Id) } @@ -1034,6 +1077,14 @@ public final class StarsTransactionsContext { } } + public var currentState: StarsTransactionsContext.State? { + var state: StarsTransactionsContext.State? + self.impl.syncWith { impl in + state = impl._state + } + return state + } + public func reload() { self.impl.with { $0.loadMore(reload: true) @@ -1051,6 +1102,14 @@ public final class StarsTransactionsContext { return StarsTransactionsContextImpl(account: account, subject: subject, mode: mode) }) } + + var peerId: EnginePeer.Id { + var peerId: EnginePeer.Id? + self.impl.syncWith { impl in + peerId = impl.peerId + } + return peerId! + } } private final class StarsSubscriptionsContextImpl { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index b2e538e6bb..9121a3cbee 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -281,6 +281,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let premiumRequired = (userFullFlags & (1 << 29)) != 0 let translationsDisabled = (userFullFlags & (1 << 23)) != 0 let adsEnabled = (userFullFlags2 & (1 << 7)) != 0 + let canViewRevenue = (userFullFlags2 & (1 << 9)) != 0 var flags: CachedUserFlags = previous.flags if premiumRequired { @@ -303,6 +304,11 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } else { flags.remove(.adsEnabled) } + if canViewRevenue { + flags.insert(.canViewRevenue) + } else { + flags.remove(.canViewRevenue) + } let callsPrivate = (userFullFlags & (1 << 5)) != 0 let canPinMessages = (userFullFlags & (1 << 7)) != 0 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index 9ef7f49317..1d87fe12af 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -582,6 +582,14 @@ func _internal_reindexCacheInBackground(account: Account, lowImpact: Bool) -> Si processResource(mediaMessages, representation.resource, MediaResourceUserContentType(file: file)) } processResource(mediaMessages, file.resource, MediaResourceUserContentType(file: file)) + for alternativeRepresentation in file.alternativeRepresentations { + if let alternativeRepresentation = alternativeRepresentation as? TelegramMediaFile { + for representation in alternativeRepresentation.previewRepresentations { + processResource(mediaMessages, representation.resource, MediaResourceUserContentType(file: file)) + } + processResource(mediaMessages, alternativeRepresentation.resource, MediaResourceUserContentType(file: file)) + } + } } else if let webpage = media as? TelegramMediaWebpage { if case let .Loaded(content) = webpage.content { if let image = content.image { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift index 90644ade6b..d21db933e6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift @@ -62,10 +62,10 @@ func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo, } } -func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool) -> Signal { +func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceRemote: Bool, ignoreCache: Bool = false) -> Signal { return postbox.transaction { transaction -> CachedStickerPackResult? in if let (info, items, local) = cachedStickerPack(transaction: transaction, reference: reference) { - if local { + if local && !ignoreCache { return .result(info, items, true) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift index e2f78fa910..8e1ae1f68d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift @@ -123,7 +123,7 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti } } -func _internal_loadedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceActualized: Bool) -> Signal { +func _internal_loadedStickerPack(postbox: Postbox, network: Network, reference: StickerPackReference, forceActualized: Bool, ignoreCache: Bool = false) -> Signal { return _internal_cachedStickerPack(postbox: postbox, network: network, reference: reference, forceRemote: forceActualized) |> map { result -> LoadedStickerPack in switch result { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 9e1d3d0a6e..2235132187 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -22,8 +22,8 @@ public extension TelegramEngine { return _internal_cachedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceRemote: forceRemote) } - public func loadedStickerPack(reference: StickerPackReference, forceActualized: Bool) -> Signal { - return _internal_loadedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceActualized: forceActualized) + public func loadedStickerPack(reference: StickerPackReference, forceActualized: Bool, ignoreCache: Bool = false) -> Signal { + return _internal_loadedStickerPack(postbox: self.account.postbox, network: self.account.network, reference: reference, forceActualized: forceActualized, ignoreCache: ignoreCache) } public func randomGreetingSticker() -> Signal { diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 7a3e295fa4..44b136b5ea 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -579,6 +579,17 @@ public extension Message { } } +public extension Message { + var pendingProcessingAttribute: PendingProcessingMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? PendingProcessingMessageAttribute { + return attribute + } + } + return nil + } +} + public extension Message { func areReactionsTags(accountPeerId: PeerId) -> Bool { if self.id.peerId == accountPeerId { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 2eef259c90..584f6b142e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -111,6 +111,24 @@ public struct PresentationResourcesSettings { drawBorder(context: context, rect: bounds) }) + public static let ton = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0) + context.addPath(path.cgPath) + context.clip() + + context.setFillColor(UIColor(rgb: 0x32ade6).cgColor) + context.fill(bounds) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonAbout"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) + } + + drawBorder(context: context, rect: bounds) + }) + public static let stars = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 5863b5a9e2..1ca5659fc2 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -34,8 +34,8 @@ private func peerMentionsAttributes(primaryTextColor: UIColor, peerIds: [(Int, E return result } -public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool) -> (text: String, spoilerRanges: [NSRange], customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)])? { - if let attributedString = universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList, forForumOverview: forForumOverview) { +public func plainServiceMessageString(strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> (text: String, spoilerRanges: [NSRange], customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)])? { + if let attributedString = universalServiceMessageString(presentationData: nil, strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: forChatList, forForumOverview: forForumOverview, forAdditionalServiceMessage: forAdditionalServiceMessage) { var ranges: [NSRange] = [] var customEmojiRanges: [(NSRange, ChatTextInputTextCustomEmojiAttribute)] = [] attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in @@ -79,7 +79,7 @@ private func peerDisplayTitles(_ peers: [Peer], strings: PresentationStrings, na } } -public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool) -> NSAttributedString? { +public func universalServiceMessageString(presentationData: (PresentationTheme, TelegramWallpaper)?, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id, forChatList: Bool, forForumOverview: Bool, forAdditionalServiceMessage: Bool = false) -> NSAttributedString? { var attributedString: NSAttributedString? let primaryTextColor: UIColor @@ -952,8 +952,11 @@ 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, _, _, _, _): - if boostPeerId == nil, let currency, let amount { + case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _, text, entities): + if !forAdditionalServiceMessage, let text { + let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) + attributedString = mutableAttributedString + } else if boostPeerId == nil, let currency, let 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]) @@ -1051,26 +1054,25 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = mutableString case .prizeStars: attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor) - case let .starGift(gift, _, nameHidden, limitNumber, limitTotal, text, entities): - let _ = nameHidden - let _ = limitNumber - let _ = limitTotal - let _ = text - let _ = entities - - 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 { - authorName = strings.Notification_StarsGift_UnknownUser - peerIds = [] - } - if message.author?.id == accountPeerId { - attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + case let .starGift(gift, _, text, entities, _, _, _): + if !forAdditionalServiceMessage, let text { + let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) + attributedString = mutableAttributedString } else { - var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) - attributes[1] = boldAttributes - attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes) + 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 { + authorName = strings.Notification_StarsGift_UnknownUser + peerIds = [] + } + if message.author?.id == accountPeerId { + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } } case .unknown: attributedString = nil diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index c337430784..f5e49e1fd6 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -10,7 +10,7 @@ public func formatTonAddress(_ address: String) -> String { return address } -public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double, dateTimeFormat: PresentationDateTimeFormat) -> String { +public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double = 1.0, dateTimeFormat: PresentationDateTimeFormat) -> String { let decimalSeparator = dateTimeFormat.decimalSeparator let normalizedValue: Double = divide ? Double(value) / 1000000000 : Double(value) var formattedValue = String(format: "%0.2f", normalizedValue * rate) @@ -27,15 +27,15 @@ public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double, return "$\(formattedValue)" } -public func formatTonAmountText(_ value: Int64, decimalSeparator: String, showPlus: Bool = false) -> String { +public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { var balanceText = "\(abs(value))" while balanceText.count < 10 { balanceText.insert("0", at: balanceText.startIndex) } - balanceText.insert(contentsOf: decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9)) + balanceText.insert(contentsOf: dateTimeFormat.decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9)) while true { if balanceText.hasSuffix("0") { - if balanceText.hasSuffix("\(decimalSeparator)0") { + if balanceText.hasSuffix("\(dateTimeFormat.decimalSeparator)0") { balanceText.removeLast() balanceText.removeLast() break @@ -46,16 +46,31 @@ public func formatTonAmountText(_ value: Int64, decimalSeparator: String, showPl break } } + + if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) { + balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dotIndex.upperBound, offsetBy: 2))]) + + let integerPartString = balanceText[.. Void let dismiss: () -> Void init( context: AccountContext, + mode: AdsInfoScreen.Mode, openPremium: @escaping () -> Void, dismiss: @escaping () -> Void ) { self.context = context + self.mode = mode self.openPremium = openPremium self.dismiss = dismiss } @@ -175,7 +186,7 @@ private final class ScrollContent: CombinedComponent { component: AnyComponent(ParagraphComponent( title: strings.AdsInfo_Respect_Title, titleColor: textColor, - text: strings.AdsInfo_Respect_Text, + text: component.mode == .bot ? strings.AdsInfo_Bot_Respect_Text : strings.AdsInfo_Respect_Text, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Ads/Privacy", @@ -187,9 +198,9 @@ private final class ScrollContent: CombinedComponent { AnyComponentWithIdentity( id: "split", component: AnyComponent(ParagraphComponent( - title: strings.AdsInfo_Split_Title, + title: component.mode == .bot ? strings.AdsInfo_Bot_Split_Title : strings.AdsInfo_Split_Title, titleColor: textColor, - text: strings.AdsInfo_Split_Text, + text: component.mode == .bot ? strings.AdsInfo_Bot_Split_Text : strings.AdsInfo_Split_Text, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Ads/Split", @@ -203,7 +214,7 @@ private final class ScrollContent: CombinedComponent { component: AnyComponent(ParagraphComponent( title: strings.AdsInfo_Ads_Title, titleColor: textColor, - text: strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string, + text: component.mode == .bot ? strings.AdsInfo_Bot_Ads_Text : strings.AdsInfo_Ads_Text("\(premiumConfiguration.minChannelRestrictAdsLevel)").string, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Premium/BoostPerk/NoAds", @@ -242,7 +253,7 @@ private final class ScrollContent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } - var infoString = strings.AdsInfo_Launch_Text + var infoString = component.mode == .bot ? strings.AdsInfo_Bot_Launch_Text : strings.AdsInfo_Launch_Text if let spaceRegex { let nsRange = NSRange(infoString.startIndex..., in: infoString) let matches = spaceRegex.matches(in: infoString, options: [], range: nsRange) @@ -328,27 +339,48 @@ private final class ScrollContent: CombinedComponent { private final class ContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment + class ExternalState { + var contentHeight: CGFloat = 0.0 + } + let context: AccountContext + let mode: AdsInfoScreen.Mode + let externalState: ExternalState let openPremium: () -> Void + let openContextMenu: () -> Void + let dismiss: () -> Void init( context: AccountContext, - openPremium: @escaping () -> Void + mode: AdsInfoScreen.Mode, + externalState: ExternalState, + openPremium: @escaping () -> Void, + openContextMenu: @escaping () -> Void, + dismiss: @escaping () -> Void ) { self.context = context + self.mode = mode + self.externalState = externalState self.openPremium = openPremium + self.openContextMenu = openContextMenu + self.dismiss = dismiss } static func ==(lhs: ContainerComponent, rhs: ContainerComponent) -> Bool { if lhs.context !== rhs.context { return false } + if lhs.mode != rhs.mode { + return false + } return true } final class State: ComponentState { var topContentOffset: CGFloat? var bottomContentOffset: CGFloat? + + var cachedMoreImage: (UIImage, PresentationTheme)? } func makeState() -> State { @@ -358,18 +390,16 @@ private final class ContainerComponent: CombinedComponent { static var body: Body { let background = Child(Rectangle.self) let scroll = Child(ScrollComponent.self) - let bottomPanel = Child(BlurredBackgroundComponent.self) - let bottomSeparator = Child(Rectangle.self) - let actionButton = Child(SolidRoundedButtonComponent.self) let scrollExternalState = ScrollComponent.ExternalState() + let moreButton = Child(Button.self) + return { context in let environment = context.environment[EnvironmentType.self] - let theme = environment.theme - let strings = environment.strings let state = context.state - let controller = environment.controller + let openContextMenu = context.component.openContextMenu + let dismiss = context.component.dismiss let background = background.update( component: Rectangle(color: environment.theme.list.plainBackgroundColor), @@ -385,9 +415,10 @@ private final class ContainerComponent: CombinedComponent { component: ScrollComponent( content: AnyComponent(ScrollContent( context: context.component.context, + mode: context.component.mode, openPremium: context.component.openPremium, dismiss: { - controller()?.dismiss() + dismiss() } )), externalState: scrollExternalState, @@ -406,129 +437,40 @@ private final class ContainerComponent: CombinedComponent { availableSize: context.availableSize, transition: context.transition ) + context.component.externalState.contentHeight = scrollExternalState.contentHeight context.add(scroll .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) - let buttonHeight: CGFloat = 50.0 - let bottomPanelPadding: CGFloat = 12.0 - let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding - let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset - - let bottomPanelAlpha: CGFloat - if scrollExternalState.contentHeight > context.availableSize.height { - if let bottomContentOffset = state.bottomContentOffset { - bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 + if case .bot = context.component.mode { + let moreImage: UIImage + if let (image, theme) = state.cachedMoreImage, theme === environment.theme { + moreImage = image } else { - bottomPanelAlpha = 1.0 + moreImage = generateMoreButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! + state.cachedMoreImage = (moreImage, environment.theme) } - } else { - bottomPanelAlpha = 0.0 + let moreButton = moreButton.update( + component: Button( + content: AnyComponent(Image(image: moreImage)), + action: { + openContextMenu() + } + ).tagged(moreTag), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(moreButton + .position(CGPoint(x: context.availableSize.width - 16.0 - moreButton.size.width / 2.0, y: 13.0 + moreButton.size.height / 2.0)) + ) } - let bottomPanel = bottomPanel.update( - component: BlurredBackgroundComponent( - color: theme.rootController.tabBar.backgroundColor - ), - availableSize: CGSize(width: context.availableSize.width, height: bottomPanelHeight), - transition: context.transition - ) - let bottomSeparator = bottomSeparator.update( - component: Rectangle( - color: theme.rootController.tabBar.separatorColor - ), - availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), - transition: context.transition - ) - - context.add(bottomPanel - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0)) - .opacity(bottomPanelAlpha) - ) - context.add(bottomSeparator - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) - .opacity(bottomPanelAlpha) - ) - - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let actionButton = actionButton.update( - component: SolidRoundedButtonComponent( - title: strings.AdsInfo_Understood, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: theme.list.itemCheckColors.fillColor, - backgroundColors: [], - foregroundColor: theme.list.itemCheckColors.foregroundColor - ), - font: .bold, - fontSize: 17.0, - height: buttonHeight, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, - action: { - controller()?.dismiss() - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), - transition: context.transition - ) - context.add(actionButton - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanelHeight + bottomPanelPadding + actionButton.size.height / 2.0)) - ) - return context.availableSize } } } -public final class AdsInfoScreen: ViewControllerComponentContainer { - private let context: AccountContext - - public init( - context: AccountContext, - forceDark: Bool = false - ) { - self.context = context - - var openPremiumImpl: (() -> Void)? - super.init( - context: context, - component: ContainerComponent( - context: context, - openPremium: { - openPremiumImpl?() - } - ), - navigationBarAppearance: .none, - statusBarStyle: .ignore, - theme: forceDark ? .dark : .default - ) - - self.navigationPresentation = .modal - - openPremiumImpl = { [weak self] in - guard let self else { - return - } - - let navigationController = self.navigationController - self.dismiss() - - Queue.mainQueue().after(0.3) { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) - navigationController?.pushViewController(controller, animated: true) - } - } - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - private final class ParagraphComponent: CombinedComponent { let title: String let titleColor: UIColor @@ -673,3 +615,939 @@ private final class ParagraphComponent: CombinedComponent { } } } + + +public class AdsInfoScreen: ViewController { + public enum Mode: Equatable { + case channel + case bot + } + + final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: AdsInfoScreen? + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + + let contentView: ComponentHostView + let footerContainerView: UIView + let footerView: ComponentHostView + + private var containerExternalState = ContainerComponent.ExternalState() + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?)? + + private let hapticFeedback = HapticFeedback() + + private var currentIsVisible: Bool = false + private var currentLayout: ContainerViewLayout? + + init(context: AccountContext, controller: AdsInfoScreen) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + if controller.forceDark { + self.presentationData = self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) + } + self.presentationData = self.presentationData.withUpdated(theme: self.presentationData.theme.withModalBlocksBackground()) + + self.controller = controller + + self.dim = ASDisplayNode() + self.dim.alpha = 0.0 + self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + + self.wrappingView = UIView() + self.containerView = UIView() + self.contentView = ComponentHostView() + + self.footerContainerView = UIView() + self.footerView = ComponentHostView() + + super.init() + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.blocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.contentView) + + self.containerView.addSubview(self.footerContainerView) + self.footerContainerView.addSubview(self.footerView) + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self.wrappedGestureRecognizerDelegate + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.controller?.dismiss(animated: true) + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let layout = self.currentLayout { + if case .regular = layout.metrics.widthClass { + return false + } + } + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + if let scrollView = otherGestureRecognizer.view as? UIScrollView { + if scrollView.contentSize.width > scrollView.contentSize.height { + return false + } + } + return true + } + return false + } + + private var isDismissing = false + func animateIn() { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) + + let targetPosition = self.containerView.center + let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + }, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void = {}) { + self.isDismissing = true + + let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in + self?.controller?.dismiss(animated: false, completion: completion) + }) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + + func requestLayout(transition: ComponentTransition) { + guard let layout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition) + } + + private var dismissOffset: CGFloat? + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, transition: ComponentTransition) { + guard !self.isDismissing else { + return + } + self.currentLayout = layout + + self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) + + let isLandscape = layout.orientation == .landscape + + var containerTopInset: CGFloat = 0.0 + let clipFrame: CGRect + if layout.metrics.widthClass == .compact { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) + if isLandscape { + self.containerView.layer.cornerRadius = 0.0 + } else { + self.containerView.layer.cornerRadius = 10.0 + } + + if #available(iOS 11.0, *) { + if layout.safeInsets.bottom.isZero { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } else { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + } + + if isLandscape { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + } else { + let coveredByModalTransition: CGFloat = 0.0 + containerTopInset = 10.0 + if let statusBarHeight = layout.statusBarHeight { + containerTopInset += statusBarHeight + } + + let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) + let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width + let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition + let maxScaledTopInset: CGFloat = containerTopInset - 10.0 + let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition + let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) + + clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) + } + } else { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + self.containerView.layer.cornerRadius = 10.0 + + let verticalInset: CGFloat = 44.0 + + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + } + + transition.setFrame(view: self.containerView, frame: clipFrame) + + var effectiveExpanded = self.isExpanded + if case .regular = layout.metrics.widthClass { + effectiveExpanded = true + } + + self.updated(transition: transition, forceUpdate: forceUpdate) + + let contentHeight = self.containerExternalState.contentHeight + if contentHeight > 0.0 && contentHeight < 400.0, let view = self.footerView.componentView as? FooterComponent.View { + view.backgroundView.alpha = 0.0 + view.separator.opacity = 0.0 + } + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + + let topInset: CGFloat + if let (panInitialTopInset, panOffset, _) = self.panGestureArguments { + if effectiveExpanded { + topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) + } else { + topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) + } + } else if let dismissOffset = self.dismissOffset, !dismissOffset.isZero { + topInset = edgeTopInset * dismissOffset + } else { + topInset = effectiveExpanded ? 0.0 : edgeTopInset + } + transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) + + let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) + + let footerHeight = self.footerHeight + let convertedFooterFrame = self.view.convert(CGRect(origin: CGPoint(x: clipFrame.minX, y: clipFrame.maxY - footerHeight), size: CGSize(width: clipFrame.width, height: footerHeight)), to: self.containerView) + transition.setFrame(view: self.footerContainerView, frame: convertedFooterFrame) + } + + func updated(transition: ComponentTransition, forceUpdate: Bool = false) { + guard let controller = self.controller, let layout = self.currentLayout else { + return + } + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + additionalInsets: layout.additionalInsets, + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: layout.metrics.orientation, + isVisible: self.currentIsVisible, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + let contentSize = self.contentView.update( + transition: transition, + component: AnyComponent( + ContainerComponent( + context: controller.context, + mode: controller.mode, + externalState: self.containerExternalState, + openPremium: { [weak self] in + guard let self, let controller = self.controller else { + return + } + + let context = controller.context + let forceDark = controller.forceDark + let navigationController = controller.navigationController + controller.dismiss(animated: true) + + Queue.mainQueue().after(0.3) { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: forceDark, dismissed: nil) + navigationController?.pushViewController(controller, animated: true) + } + }, + openContextMenu: { [weak self] in + guard let self else { + return + } + self.infoPressed() + }, + dismiss: { [weak self] in + guard let self, let controller = self.controller else { + return + } + controller.dismiss(animated: true) + } + ) + ), + environment: { environment }, + forceUpdate: forceUpdate, + containerSize: self.containerView.bounds.size + ) + self.contentView.frame = CGRect(origin: .zero, size: contentSize) + + let footerHeight = self.footerHeight + let footerSize = self.footerView.update( + transition: .immediate, + component: AnyComponent( + FooterComponent( + context: controller.context, + theme: self.presentationData.theme, + title: self.presentationData.strings.AdsInfo_Understood, + action: { [weak self] in + guard let self else { + return + } + self.buttonPressed() + } + ) + ), + environment: {}, + containerSize: CGSize(width: self.containerView.bounds.width, height: footerHeight) + ) + self.footerView.frame = CGRect(origin: .zero, size: footerSize) + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let layout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var footerHeight: CGFloat { + guard let layout = self.currentLayout else { + return 58.0 + } + + var footerHeight: CGFloat = 8.0 + 50.0 + footerHeight += layout.intrinsicInsets.bottom > 0.0 ? layout.intrinsicInsets.bottom + 5.0 : 8.0 + return footerHeight + } + + private var defaultTopInset: CGFloat { + guard let layout = self.currentLayout else { + return 210.0 + } + if case .compact = layout.metrics.widthClass { + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = layout.intrinsicInsets.bottom > 0.0 ? layout.intrinsicInsets.bottom + 5.0 : bottomPanelPadding + let panelHeight: CGFloat = bottomPanelPadding + 50.0 + bottomInset + 28.0 + + var defaultTopInset = layout.size.height - layout.size.width - 128.0 - panelHeight + + let containerTopInset = 10.0 + (layout.statusBarHeight ?? 0.0) + let contentHeight = self.containerExternalState.contentHeight + let footerHeight = self.footerHeight + if contentHeight > 0.0 { + let delta = (layout.size.height - defaultTopInset - containerTopInset) - contentHeight - footerHeight - 16.0 + if delta > 0.0 { + defaultTopInset += delta + } + } + return defaultTopInset + } else { + return 210.0 + } + } + + private func findVerticalScrollView(view: UIView?) -> UIScrollView? { + if let view = view { + if let view = view as? UIScrollView, view.contentSize.height > view.contentSize.width { + return view + } + return findVerticalScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let layout = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollView = self.findVerticalScrollView(view: currentHitView) + if scrollView?.frame.height == self.frame.width { + scrollView = nil + } + if scrollView?.isDescendant(of: self.view) == false { + scrollView = nil + } + + let topInset: CGFloat + if self.isExpanded { + topInset = 0.0 + } else { + topInset = edgeTopInset + } + + self.panGestureArguments = (topInset, 0.0, scrollView) + case .changed: + guard let (topInset, panOffset, scrollView) = self.panGestureArguments else { + return + } + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + var translation = recognizer.translation(in: self.view).y + + var currentOffset = topInset + translation + + let epsilon = 1.0 + if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } else if let scrollView = scrollView { + translation = panOffset + currentOffset = topInset + translation + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.view) + } else if currentOffset > 0.0 { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + } + + if scrollView == nil { + translation = max(0.0, translation) + } + + self.panGestureArguments = (topInset, translation, scrollView) + + if !self.isExpanded { + if currentOffset > 0.0, let scrollView = scrollView { + scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + self.bounds = bounds + + self.containerLayoutUpdated(layout: layout, transition: .immediate) + case .ended: + guard let (currentTopInset, panOffset, scrollView) = self.panGestureArguments else { + return + } + self.panGestureArguments = nil + + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + let translation = recognizer.translation(in: self.view).y + var velocity = recognizer.velocity(in: self.view) + + if self.isExpanded { + if contentOffset > 0.1 { + velocity = CGPoint() + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + + scrollView?.bounces = true + + let offset = currentTopInset + panOffset + let topInset: CGFloat = edgeTopInset + + var dismissing = false + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { + self.controller?.dismiss(animated: true, completion: nil) + dismissing = true + } else if self.isExpanded { + if velocity.y > 300.0 || offset > topInset / 2.0 { + self.isExpanded = false + if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + let distance = topInset - offset + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if scrollView != nil, (velocity.y < -300.0 || offset < topInset / 2.0) { + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } else { + if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) + default: + break + } + } + + func updateDismissOffset(_ offset: CGFloat) { + guard self.isExpanded, let layout = self.currentLayout else { + return + } + + self.dismissOffset = offset + self.containerLayoutUpdated(layout: layout, transition: .immediate) + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.dismissOffset = nil + self.isExpanded = isExpanded + + guard let layout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } + + func displayUndo(_ content: UndoOverlayContent) { + guard let controller = self.controller else { + return + } + let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return true + }), in: .current) + } + + func infoPressed() { + guard let referenceView = self.contentView.findTaggedView(tag: moreTag), let controller = self.controller, let message = controller.message, let adAttribute = message.adAttribute else { + return + } + + let context = controller.context + let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } + + var actions: [ContextMenuItem] = [] + if adAttribute.sponsorInfo != nil || adAttribute.additionalInfo != nil { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfo, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Channels"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + var subItems: [ContextMenuItem] = [] + + subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, textColor: .primary, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, iconPosition: .left, action: { c, _ in + c?.popItems() + }))) + + subItems.append(.separator) + + if let sponsorInfo = adAttribute.sponsorInfo { + subItems.append(.action(ContextMenuActionItem(text: sponsorInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return nil + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + UIPasteboard.general.string = sponsorInfo + + self?.displayUndo(.copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)) + }) + }))) + } + if let additionalInfo = adAttribute.additionalInfo { + subItems.append(.action(ContextMenuActionItem(text: additionalInfo, textColor: .primary, textLayout: .multiline, textFont: .custom(font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 0.8)), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return nil + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + UIPasteboard.general.string = additionalInfo + + self?.displayUndo(.copy(text: presentationData.strings.Chat_ContextMenu_AdSponsorInfoCopied)) + }) + }))) + } + + c?.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) + }))) + } + + let removeAd = self.controller?.removeAd + if adAttribute.canReport { + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] _, f in + f(.default) + + guard let navigationController = self?.controller?.navigationController as? NavigationController else { + return + } + + self?.controller?.dismiss(animated: true) + + let _ = (context.engine.messages.reportAdMessage(peerId: message.id.peerId, opaqueId: adAttribute.opaqueId, option: nil) + |> deliverOnMainQueue).start(next: { [weak navigationController] result in + if case let .options(title, options) = result { + Queue.mainQueue().after(0.2) { + navigationController?.pushViewController( + AdsReportScreen( + context: context, + peerId: message.id.peerId, + opaqueId: adAttribute.opaqueId, + title: title, + options: options, + completed: { + removeAd?(adAttribute.opaqueId) + } + ) + ) + } + } + }) + }))) + + actions.append(.separator) + + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_RemoveAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + if context.isPremium { + removeAd?(adAttribute.opaqueId) + } else { + self?.presentNoAdsDemo() + } + }) + }))) + } else { + if !actions.isEmpty { + actions.append(.separator) + } + actions.append(.action(ContextMenuActionItem(text: presentationData.strings.SponsoredMessageMenu_Hide, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.primaryTextColor) + }, iconSource: nil, action: { [weak self] c, _ in + c?.dismiss(completion: { + if context.isPremium { + removeAd?(adAttribute.opaqueId) + } else { + self?.presentNoAdsDemo() + } + }) + }))) + } + + let contextController = ContextController(presentationData: presentationData, source: .reference(AdsInfoContextReferenceContentSource(controller: controller, sourceView: referenceView, insets: .zero, contentInsets: .zero)), items: .single(ContextController.Items(content: .list(actions))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + + func presentNoAdsDemo() { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + let context = controller.context + var replaceImpl: ((ViewController) -> Void)? + let demoController = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .ads, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak demoController] c in + demoController?.replace(with: c) + } + controller.dismiss(animated: true) + Queue.mainQueue().after(0.4) { + navigationController.pushViewController(demoController) + } + } + + func buttonPressed() { + self.controller?.dismiss(animated: true) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let mode: Mode + private let message: Message? + private let forceDark: Bool + + private var currentLayout: ContainerViewLayout? + + public var removeAd: (Data) -> Void = { _ in } + + public init( + context: AccountContext, + mode: Mode, + message: Message? = nil, + forceDark: Bool = false + ) { + self.context = context + self.mode = mode + self.message = message + self.forceDark = forceDark + + super.init(navigationBarPresentationData: nil) + + self.navigationPresentation = .flatModal + self.statusBar.statusBarStyle = .Ignore + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self) + self.displayNodeDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + self.view.endEditing(true) + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { + super.dismiss(animated: false, completion: {}) + completion?() + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + self.node.containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition)) + } +} + +private final class FooterComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let title: String + let action: () -> Void + + init(context: AccountContext, theme: PresentationTheme, title: String, action: @escaping () -> Void) { + self.context = context + self.theme = theme + self.title = title + self.action = action + } + + static func ==(lhs: FooterComponent, rhs: FooterComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + let backgroundView: BlurredBackgroundView + let separator = SimpleLayer() + + private let button = ComponentView() + + private var component: FooterComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: nil) + + super.init(frame: frame) + + self.backgroundView.clipsToBounds = true + + self.addSubview(self.backgroundView) + self.layer.addSublayer(self.separator) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: FooterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let bounds = CGRect(origin: .zero, size: availableSize) + + self.backgroundView.updateColor(color: component.theme.rootController.tabBar.backgroundColor, transition: transition.containedViewLayoutTransition) + self.backgroundView.update(size: bounds.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.backgroundView, frame: bounds) + + self.separator.backgroundColor = component.theme.rootController.tabBar.separatorColor.cgColor + transition.setFrame(layer: self.separator, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + let buttonSize = self.button.update( + transition: .immediate, + component: AnyComponent( + SolidRoundedButtonComponent( + title: component.title, + theme: SolidRoundedButtonComponent.Theme(theme: component.theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + animationName: nil, + iconPosition: .left, + action: { + component.action() + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 32.0, height: availableSize.height) + ) + + if let view = self.button.view { + if view.superview == nil { + self.addSubview(view) + } + let buttonFrame = CGRect(origin: CGPoint(x: 16.0, y: 8.0), size: buttonSize) + view.frame = buttonFrame + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateMoreButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(foregroundColor.cgColor) + + let circleSize = CGSize(width: 4.0, height: 4.0) + context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0), y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0) - circleSize.width - 3.0, y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) + + context.fillEllipse(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.height - circleSize.width) / 2.0) + circleSize.width + 3.0, y: floorToScreenPixels((size.height - circleSize.height) / 2.0)), size: circleSize)) + }) +} + +private final class AdsInfoContextReferenceContentSource: ContextReferenceContentSource { + let controller: ViewController + let sourceView: UIView + let insets: UIEdgeInsets + let contentInsets: UIEdgeInsets + + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { + self.controller = controller + self.sourceView = sourceView + self.insets = insets + self.contentInsets = contentInsets + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) + } +} diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD b/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD index 53ec1414a6..f4ce434cc1 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/BUILD @@ -13,6 +13,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift index bb26607690..b3ead20bb7 100644 --- a/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift +++ b/submodules/TelegramUI/Components/AnimatedTextComponent/Sources/AnimatedTextComponent.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramPresentationData public final class AnimatedTextComponent: Component { public struct Item: Equatable { @@ -24,15 +25,18 @@ public final class AnimatedTextComponent: Component { public let font: UIFont public let color: UIColor public let items: [Item] + public let noDelay: Bool public init( font: UIFont, color: UIColor, - items: [Item] + items: [Item], + noDelay: Bool = false ) { self.font = font self.color = color self.items = items + self.noDelay = noDelay } public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool { @@ -45,6 +49,9 @@ public final class AnimatedTextComponent: Component { if lhs.items != rhs.items { return false } + if lhs.noDelay != rhs.noDelay { + return false + } return true } @@ -157,10 +164,12 @@ public final class AnimatedTextComponent: Component { if animateIn, !transition.animation.isImmediate { var delayWidth: Double = 0.0 - if let firstDelayWidth { - delayWidth = size.width - firstDelayWidth - } else { - firstDelayWidth = size.width + if !component.noDelay { + if let firstDelayWidth { + delayWidth = size.width - firstDelayWidth + } else { + firstDelayWidth = size.width + } } characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring) @@ -220,3 +229,33 @@ public final class AnimatedTextComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public extension AnimatedTextComponent { + static func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { + var textItems: [AnimatedTextComponent.Item] = [] + + var previousIndex = 0 + let nsString = string.string as NSString + for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { + if range.range.lowerBound > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) + } + if let value = mapping[range.index] { + let isUnbreakable: Bool + switch value { + case .text: + isUnbreakable = true + case .number: + isUnbreakable = false + } + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) + } + previousIndex = range.range.upperBound + } + if nsString.length > previousIndex { + textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) + } + + return textItems + } +} diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 29fcb45f51..7a8fb12ea5 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -17,9 +17,9 @@ private func alignUp(size: Int, align: Int) -> Int { private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { if useTotalFileAllocatedSize { let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) { + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { if values.isRegularFile ?? false { - if let fileSize = values.totalFileAllocatedSize { + if let fileSize = values.fileAllocatedSize { return Int64(fileSize) } } diff --git a/submodules/TelegramUI/Components/BadgeComponent/BUILD b/submodules/TelegramUI/Components/BadgeComponent/BUILD new file mode 100644 index 0000000000..023edf4bb5 --- /dev/null +++ b/submodules/TelegramUI/Components/BadgeComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BadgeComponent", + module_name = "BadgeComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/RasterizedCompositionComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/BadgeComponent/Sources/BadgeComponent.swift b/submodules/TelegramUI/Components/BadgeComponent/Sources/BadgeComponent.swift new file mode 100644 index 0000000000..be4b3b3042 --- /dev/null +++ b/submodules/TelegramUI/Components/BadgeComponent/Sources/BadgeComponent.swift @@ -0,0 +1,184 @@ +import Foundation +import UIKit +import Display +import RasterizedCompositionComponent +import ComponentFlow + +public final class BadgeComponent: Component { + public let text: String + public let font: UIFont + public let cornerRadius: CGFloat + public let insets: UIEdgeInsets + public let outerInsets: UIEdgeInsets + + public init( + text: String, + font: UIFont, + cornerRadius: CGFloat, + insets: UIEdgeInsets, + outerInsets: UIEdgeInsets + ) { + self.text = text + self.font = font + self.cornerRadius = cornerRadius + self.insets = insets + self.outerInsets = outerInsets + } + + public static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.font != rhs.font { + return false + } + if lhs.cornerRadius != rhs.cornerRadius { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.outerInsets != rhs.outerInsets { + return false + } + return true + } + + private struct TextLayout { + var size: CGSize + var opticalBounds: CGRect + + init(size: CGSize, opticalBounds: CGRect) { + self.size = size + self.opticalBounds = opticalBounds + } + } + + public final class View: UIView { + override public static var layerClass: AnyClass { + return RasterizedCompositionLayer.self + } + + private let contentsClippingLayer: RasterizedCompositionLayer + private let backgroundInsetLayer: RasterizedCompositionImageLayer + private let backgroundLayer: RasterizedCompositionImageLayer + private let textContentsLayer: RasterizedCompositionImageLayer + + private var textLayout: TextLayout? + + private var component: BadgeComponent? + + override public init(frame: CGRect) { + self.contentsClippingLayer = RasterizedCompositionLayer() + self.backgroundInsetLayer = RasterizedCompositionImageLayer() + self.backgroundLayer = RasterizedCompositionImageLayer() + + self.textContentsLayer = RasterizedCompositionImageLayer() + self.textContentsLayer.anchorPoint = CGPoint() + + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundInsetLayer) + self.layer.addSublayer(self.backgroundLayer) + self.layer.addSublayer(self.contentsClippingLayer) + self.contentsClippingLayer.addSublayer(self.textContentsLayer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + + if component.text != previousComponent?.text || component.font != previousComponent?.font { + let attributedText = NSAttributedString(string: component.text, attributes: [ + NSAttributedString.Key.font: component.font, + NSAttributedString.Key.foregroundColor: UIColor.black + ]) + + var boundingRect = attributedText.boundingRect(with: availableSize, options: .usesLineFragmentOrigin, context: nil) + boundingRect.size.width = ceil(boundingRect.size.width) + boundingRect.size.height = ceil(boundingRect.size.height) + + if let context = DrawingContext(size: boundingRect.size, scale: 0.0, opaque: false, clear: true) { + context.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + + attributedText.draw(at: CGPoint()) + } + var minFilledLineY = Int(context.scaledSize.height) - 1 + var maxFilledLineY = 0 + var minFilledLineX = Int(context.scaledSize.width) - 1 + var maxFilledLineX = 0 + for y in 0 ..< Int(context.scaledSize.height) { + let linePtr = context.bytes.advanced(by: max(0, y) * context.bytesPerRow).assumingMemoryBound(to: UInt32.self) + + for x in 0 ..< Int(context.scaledSize.width) { + let pixelPtr = linePtr.advanced(by: x) + if pixelPtr.pointee != 0 { + minFilledLineY = min(y, minFilledLineY) + maxFilledLineY = max(y, maxFilledLineY) + minFilledLineX = min(x, minFilledLineX) + maxFilledLineX = max(x, maxFilledLineX) + } + } + } + + var opticalBounds = CGRect() + if minFilledLineX <= maxFilledLineX && minFilledLineY <= maxFilledLineY { + opticalBounds.origin.x = CGFloat(minFilledLineX) / context.scale + opticalBounds.origin.y = CGFloat(minFilledLineY) / context.scale + opticalBounds.size.width = CGFloat(maxFilledLineX - minFilledLineX) / context.scale + opticalBounds.size.height = CGFloat(maxFilledLineY - minFilledLineY) / context.scale + } + + self.textContentsLayer.image = context.generateImage() + self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: opticalBounds) + } else { + self.textLayout = TextLayout(size: boundingRect.size, opticalBounds: CGRect(origin: CGPoint(), size: boundingRect.size)) + } + } + + if component.cornerRadius != previousComponent?.cornerRadius { + self.backgroundLayer.image = generateStretchableFilledCircleImage(diameter: component.cornerRadius * 2.0, color: .white) + + self.backgroundInsetLayer.image = generateStretchableFilledCircleImage(diameter: component.cornerRadius * 2.0, color: .black) + } + + let textSize = self.textLayout?.size ?? CGSize(width: 1.0, height: 1.0) + + let size = CGSize(width: textSize.width + component.insets.left + component.insets.right, height: textSize.height + component.insets.top + component.insets.bottom) + + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) + transition.setFrame(layer: self.contentsClippingLayer, frame: backgroundFrame) + + let outerInsetsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX - component.outerInsets.left, y: backgroundFrame.minY - component.outerInsets.top), size: CGSize(width: backgroundFrame.width + component.outerInsets.left + component.outerInsets.right, height: backgroundFrame.height + component.outerInsets.top + component.outerInsets.bottom)) + transition.setFrame(layer: self.backgroundInsetLayer, frame: outerInsetsFrame) + + var textFrame = CGRect(origin: CGPoint(x: component.insets.left, y: component.insets.top), size: textSize) + if let textLayout = self.textLayout { + textFrame.origin.x = -textLayout.opticalBounds.minX + floorToScreenPixels((backgroundFrame.width - textLayout.opticalBounds.width) * 0.5) + textFrame.origin.y = -textLayout.opticalBounds.minY + floorToScreenPixels((backgroundFrame.height - textLayout.opticalBounds.height) * 0.5) + } + + transition.setPosition(layer: self.textContentsLayer, position: textFrame.origin) + self.textContentsLayer.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift index 5109dc625c..a097d80a07 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -219,6 +219,7 @@ open class ChatMessageBubbleContentNode: ASDisplayNode { public var item: ChatMessageBubbleContentItem? public var updateIsTextSelectionActive: ((Bool) -> Void)? + public var requestInlineUpdate: (() -> Void)? open var disablesClipping: Bool { return false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 601a067d4b..6bbd6909d7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -131,8 +131,13 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } } + var messageMedia = message.media + if let updatingMedia = itemAttributes.updatingMedia, messageMedia.isEmpty, case let .update(media) = updatingMedia.media { + messageMedia.append(media.media) + } + var isFile = false - inner: for media in message.media { + inner: for media in messageMedia { if let media = media as? TelegramMediaPaidContent { var index = 0 for _ in media.extendedMedia { @@ -704,6 +709,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var forceStopAnimations: Bool = false + typealias Params = (item: ChatMessageItem, params: ListViewItemLayoutParams, mergedTop: ChatMessageMerge, mergedBottom: ChatMessageMerge, dateHeaderAtBottom: Bool) + private var currentInputParams: Params? + private var currentApplyParams: ListViewItemApply? + required public init(rotated: Bool) { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() @@ -1278,24 +1287,22 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } recognizer.highlight = { [weak self] point in - if let strongSelf = self { - if strongSelf.selectionNode == nil { - if let replyInfoNode = strongSelf.replyInfoNode { - var translatedPoint: CGPoint? - let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view) - if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { - translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view) - } - replyInfoNode.updateTouchesAtPoint(translatedPoint) + if let strongSelf = self, strongSelf.selectionNode == nil { + if let replyInfoNode = strongSelf.replyInfoNode { + var translatedPoint: CGPoint? + let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view) + if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view) } - if let forwardInfoNode = strongSelf.forwardInfoNode { - var translatedPoint: CGPoint? - let convertedNodeFrame = forwardInfoNode.view.convert(forwardInfoNode.bounds, to: strongSelf.view) - if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { - translatedPoint = strongSelf.view.convert(point, to: forwardInfoNode.view) - } - forwardInfoNode.updateTouchesAtPoint(translatedPoint) + replyInfoNode.updateTouchesAtPoint(translatedPoint) + } + if let forwardInfoNode = strongSelf.forwardInfoNode { + var translatedPoint: CGPoint? + let convertedNodeFrame = forwardInfoNode.view.convert(forwardInfoNode.bounds, to: strongSelf.view) + if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) { + translatedPoint = strongSelf.view.convert(point, to: forwardInfoNode.view) } + forwardInfoNode.updateTouchesAtPoint(translatedPoint) } for contentNode in strongSelf.contentNodes { var translatedPoint: CGPoint? @@ -1360,6 +1367,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } + private func internalUpdateLayout() { + if let inputParams = self.currentInputParams, let currentApplyParams = self.currentApplyParams { + let (_, applyLayout) = self.asyncLayout()(inputParams.item, inputParams.params, inputParams.mergedTop, inputParams.mergedBottom, inputParams.dateHeaderAtBottom) + applyLayout(.None, ListViewItemApply(isOnScreen: currentApplyParams.isOnScreen, timestamp: nil), false) + } + } + override public func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { var currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = [] for contentNode in self.contentNodes { @@ -1418,7 +1432,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } private static func beginLayout( - selfReference: Weak, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool, + selfReference: Weak, + _ item: ChatMessageItem, + _ params: ListViewItemLayoutParams, + _ mergedTop: ChatMessageMerge, + _ mergedBottom: ChatMessageMerge, + _ dateHeaderAtBottom: Bool, currentContentClassesPropertiesAndLayouts: [(Message, AnyClass, Bool, Int?, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))], authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), viaMeasureLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), @@ -1542,11 +1561,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info { - if info.flags.contains(.messagesShouldHaveProfiles) { + if info.flags.contains(.messagesShouldHaveProfiles) && !item.presentationData.isPreview { var allowAuthor = incoming overrideEffectiveAuthor = true - if let author = firstMessage.author, author is TelegramChannel, !incoming || item.presentationData.isPreview { + if let author = firstMessage.author, author is TelegramChannel, !incoming { allowAuthor = true ignoreNameHiding = true } @@ -3092,6 +3111,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return (layout, { animation, applyInfo, synchronousLoads in return ChatMessageBubbleItemNode.applyLayout(selfReference: selfReference, animation, synchronousLoads, + inputParams: (item, params, mergedTop, mergedBottom, dateHeaderAtBottom), params: params, applyInfo: applyInfo, layout: layout, @@ -3150,6 +3170,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private static func applyLayout(selfReference: Weak, _ animation: ListViewItemUpdateAnimation, _ synchronousLoads: Bool, + inputParams: Params, params: ListViewItemLayoutParams, applyInfo: ListViewItemApply, layout: ListViewItemNodeLayout, @@ -3206,6 +3227,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return } + strongSelf.currentInputParams = inputParams + strongSelf.currentApplyParams = applyInfo + if item.message.id.namespace == Namespaces.Message.Local || item.message.id.namespace == Namespaces.Message.ScheduledLocal || item.message.id.namespace == Namespaces.Message.QuickReplyLocal { strongSelf.wasPending = true } @@ -4021,6 +4045,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in contextSourceNode?.updateDistractionFreeMode?(value) } + contentNode.requestInlineUpdate = { [weak strongSelf] in + guard let strongSelf else { + return + } + + strongSelf.internalUpdateLayout() + } contentNode.updateIsExtractedToContextPreview(contextSourceNode.isExtractedToContextPreview) } } @@ -4998,13 +5029,24 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil case .longTap, .doubleTap, .secondaryTap: if let item = self.item, self.backgroundNode.frame.contains(location) { -// let message = item.message - if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) { return .action(InternalBubbleTapAction.Action {}) } if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) { - return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: item.content.firstMessage, selectAll: false, subFrame: self.backgroundNode.frame, disableDefaultPressAnimation: true)) + if self.selectionNode != nil, let attribute = item.message.attributes.first(where: { $0 is ReplyMessageAttribute }) as? ReplyMessageAttribute { + return .action(InternalBubbleTapAction.Action({ [weak self] in + guard let self else { + return + } + var progress: Promise? + if let replyInfoNode = self.replyInfoNode { + progress = replyInfoNode.makeProgress() + } + item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.isQuote ? attribute.quote.flatMap { quote in NavigateToMessageParams.Quote(string: quote.text, offset: quote.offset) } : nil, progress: progress)) + }, contextMenuOnLongPress: true)) + } else { + return .openContextMenu(InternalBubbleTapAction.OpenContextMenu(tapMessage: item.content.firstMessage, selectAll: false, subFrame: self.backgroundNode.frame, disableDefaultPressAnimation: true)) + } } var tapMessage: Message? = item.content.firstMessage @@ -5083,12 +5125,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) - case let .hashtag(_, hashtag): + case let .hashtag(peerName, hashtag): + var fullHashtag = hashtag + if let peerName { + fullHashtag += "@\(peerName)" + } return .action(InternalBubbleTapAction.Action { [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(hashtag, rects: rects) else { + guard let self, let contentNode = self.contextContentNodeForLink(fullHashtag, rects: rects) else { return } - item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) + item.controllerInteraction.longTap(.hashtag(fullHashtag), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case .instantPage: break @@ -5442,10 +5488,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let selectionNode = ChatMessageSelectionNode(wallpaper: item.presentationData.theme.wallpaper, theme: item.presentationData.theme.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { switch item.content { - case let .message(message, _, _, _, _): + case let .message(message, _, _, _, _): item.controllerInteraction.toggleMessagesSelection([message.id], value) - case let .group(messages): - item.controllerInteraction.toggleMessagesSelection(messages.map { $0.0.id }, value) + case let .group(messages): + item.controllerInteraction.toggleMessagesSelection(messages.map { $0.0.id }, value) } } }) @@ -6076,14 +6122,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } override public func getStatusNode() -> ASDisplayNode? { + if let statusNode = self.mosaicStatusNode { + return statusNode + } for contentNode in self.contentNodes { if let statusNode = contentNode.getStatusNode() { return statusNode } } - if let statusNode = self.mosaicStatusNode { - return statusNode - } return nil } @@ -6224,7 +6270,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } for contentNode in self.contentNodes { - if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode { + if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode || contentNode is ChatMessageWebpageBubbleContentNode || contentNode is ChatMessageInvoiceBubbleContentNode || contentNode is ChatMessageGameBubbleContentNode || contentNode is ChatMessageInstantVideoBubbleContentNode { contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) } else { contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift index 7d076a23c5..f5510b8649 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/ChatMessageDateAndStatusNode.swift @@ -362,7 +362,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode { selectedStarsForeground: themeColors.reactionStarsActiveForeground.argb, extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb, extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, - extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb, + extractedSelectedForeground: arguments.presentationData.theme.theme.overallDarkAppearance ? themeColors.reactionActiveForeground.argb : arguments.presentationData.theme.theme.contextMenu.primaryColor.argb, deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb, selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index 251d880612..a751806322 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -70,7 +70,7 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess return strings.Message_RecommendedLabel } } - + var timestamp: Int32 if let scheduleTime = message.scheduleTime { timestamp = scheduleTime @@ -95,6 +95,10 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess dateText = " " } + if message.id.namespace == Namespaces.Message.ScheduledCloud, let _ = message.pendingProcessingAttribute { + return "appx. \(dateText)" + } + if displayFullDate { let dayText: String diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift index d52d60c761..30bb04bf59 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift @@ -106,6 +106,9 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { selectedFile = telegramFile } } + if let updatingMedia = item.attributes.updatingMedia, case let .update(media) = updatingMedia.media, let file = media.media as? TelegramMediaFile { + selectedFile = file + } var incoming = item.message.effectivelyIncoming(item.context.account.peerId) if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { @@ -135,7 +138,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { } } - let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!) + let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile) let (initialWidth, refineLayout) = interactiveFileLayout(ChatMessageInteractiveFileNode.Arguments( context: item.context, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index fc09e566ff..b4eaa8b264 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -24,7 +24,7 @@ import TextNodeWithEntities import InvisibleInkDustNode private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { - return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false) + return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true) } public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index ada3bff052..7d9888408e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -303,7 +303,12 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } @objc private func progressPressed() { - if let resourceStatus = self.resourceStatus { + if let _ = self.arguments?.attributes.updatingMedia { + if let message = self.message { + self.context?.account.pendingUpdateMessageManager.cancel(messageId: message.id) + } + } + else if let resourceStatus = self.resourceStatus { switch resourceStatus.mediaStatus { case let .fetchStatus(fetchStatus): if let context = self.context, let message = self.message, message.flags.isSending { @@ -590,10 +595,15 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { statusUpdated = true } - let hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo + var hasThumbnail = (!arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil) && !arguments.file.isMusic && !arguments.file.isVoice && !arguments.file.isInstantVideo + var hasThumbnailImage = !arguments.file.previewRepresentations.isEmpty || arguments.file.immediateThumbnailData != nil + if case let .update(media) = arguments.attributes.updatingMedia?.media, let file = media.media as? TelegramMediaFile { + hasThumbnail = largestImageRepresentation(file.previewRepresentations) != nil || file.immediateThumbnailData != nil || file.mimeType.hasPrefix("image/") + hasThumbnailImage = hasThumbnail + } if mediaUpdated { - if largestImageRepresentation(arguments.file.previewRepresentations) != nil || arguments.file.immediateThumbnailData != nil { + if hasThumbnailImage { updateImageSignal = chatMessageImageFile(account: arguments.context.account, userLocation: .peer(arguments.message.id.peerId), fileReference: .message(message: MessageReference(arguments.message), media: arguments.file), thumbnail: true) } @@ -1622,59 +1632,64 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } - switch resourceStatus.mediaStatus { - case var .fetchStatus(fetchStatus): - if self.message?.forwardInfo != nil { - fetchStatus = resourceStatus.fetchStatus - } - (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false - - switch fetchStatus { - case let .Fetching(_, progress): - let adjustedProgress = max(progress, 0.027) - var wasCheck = false - if let statusNode = self.statusNode, case .check = statusNode.state { - wasCheck = true + if let updatingMedia = arguments.attributes.updatingMedia, case .update = updatingMedia.media { + let adjustedProgress = max(CGFloat(updatingMedia.progress), 0.027) + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + } else { + switch resourceStatus.mediaStatus { + case var .fetchStatus(fetchStatus): + if self.message?.forwardInfo != nil { + fetchStatus = resourceStatus.fetchStatus } + (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false - if isAudio && !isVoice && !isSending { - state = .play - } else { - if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { - state = .check(appearance: nil) + switch fetchStatus { + case let .Fetching(_, progress): + let adjustedProgress = max(progress, 0.027) + var wasCheck = false + if let statusNode = self.statusNode, case .check = statusNode.state { + wasCheck = true + } + + if isAudio && !isVoice && !isSending { + state = .play } else { - state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) { + state = .check(appearance: nil) + } else { + state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil) + } + } + case .Local: + if isAudio { + state = .play + } else if let fileIconImage = self.fileIconImage { + state = .customIcon(fileIconImage) + } else { + state = .none + } + case .Remote, .Paused: + if isAudio && !isVoice { + state = .play + } else { + state = .download } } - case .Local: - if isAudio { - state = .play - } else if let fileIconImage = self.fileIconImage { - state = .customIcon(fileIconImage) + case let .playbackStatus(playbackStatus): + (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage + + if isViewOnceMessage && playbackStatus == .playing { + state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel)) + if incoming { + self.consumableContentNode.isHidden = true + } } else { - state = .none - } - case .Remote, .Paused: - if isAudio && !isVoice { - state = .play - } else { - state = .download - } - } - case let .playbackStatus(playbackStatus): - (self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage - - if isViewOnceMessage && playbackStatus == .playing { - state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel)) - if incoming { - self.consumableContentNode.isHidden = true - } - } else { - switch playbackStatus { - case .playing: - state = .pause - case .paused: - state = .play + switch playbackStatus { + case .playing: + state = .pause + case .paused: + state = .play + } } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 6cfd0d6f48..41c240d223 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -513,6 +513,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr public var updateMessageReaction: ((Message, ChatControllerInteractionReaction, Bool, ContextExtractedContentContainingView?) -> Void)? public var playMessageEffect: ((Message) -> Void)? public var activateAgeRestrictedMedia: (() -> Void)? + public var requestInlineUpdate: (() -> Void)? + + private var hlsInlinePlaybackRange: Range? + private var appliedHlsInlinePlaybackRange: Range? + private var hlsInlinePlaybackRangeDisposable: Disposable? override public init() { self.pinchContainerNode = PinchSourceContainerNode() @@ -618,10 +623,15 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr self.playerStatusDisposable.dispose() self.fetchDisposable.dispose() self.secretTimer?.invalidate() + self.hlsInlinePlaybackRangeDisposable?.dispose() } public func isAvailableForGalleryTransition() -> Bool { - return self.automaticPlayback ?? false + if let automaticPlayback = self.automaticPlayback, automaticPlayback, self.decoration != nil { + return true + } else { + return false + } } public func isAvailableForInstantPageTransition() -> Bool { @@ -779,6 +789,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback + let hlsInlinePlaybackRange = self.hlsInlinePlaybackRange + let appliedHlsInlinePlaybackRange = self.appliedHlsInlinePlaybackRange + return { [weak self] context, presentationData, dateTimeFormat, message, associatedData, attributes, media, mediaIndex, dateAndStatus, automaticDownload, peerType, peerId, sizeCalculation, layoutConstants, contentMode, presentationContext in let _ = peerType @@ -1085,6 +1098,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } else { mediaUpdated = true } + let inlinePlaybackRangeUpdated = hlsInlinePlaybackRange != appliedHlsInlinePlaybackRange var isSendingUpdated = false if let currentMessage = currentMessage { @@ -1107,6 +1121,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr var updateAnimatedStickerFile: TelegramMediaFile? var onlyFullSizeVideoThumbnail: Bool? + var loadHLSRangeVideoFile: TelegramMediaFile? + var emptyColor: UIColor var patternArguments: PatternWallpaperArguments? if isSticker { @@ -1140,7 +1156,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated { + let reloadMedia = mediaUpdated || isSendingUpdated || automaticPlaybackUpdated + if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated || inlinePlaybackRangeUpdated { var media = media var extendedMedia: TelegramExtendedMedia? @@ -1216,7 +1233,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: image, resource: resource) } }) - } else if let file = media as? TelegramMediaFile { + } else if var file = media as? TelegramMediaFile { if isSecretMedia { updateImageSignal = { synchronousLoad, _ in return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) @@ -1248,20 +1265,34 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading { - updateVideoFile = file - if hasCurrentVideoNode { - if let currentFile = currentMedia as? TelegramMediaFile { - if currentFile.resource is EmptyMediaResource { - replaceVideoNode = true - } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { - replaceVideoNode = true - } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { - replaceVideoNode = true - } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { - replaceVideoNode = true - } + loadHLSRangeVideoFile = file + + var passFile = true + if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) { + file = minimizedQualityFile.file.media + if hlsInlinePlaybackRange == nil { + passFile = false } - } else if !(file.resource is LocalFileVideoMediaResource) { + } + + if passFile { + updateVideoFile = file + if hasCurrentVideoNode { + if let currentFile = currentMedia as? TelegramMediaFile { + if currentFile.resource is EmptyMediaResource { + replaceVideoNode = true + } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { + replaceVideoNode = true + } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { + replaceVideoNode = true + } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { + replaceVideoNode = true + } + } + } else if !(file.resource is LocalFileVideoMediaResource) { + replaceVideoNode = true + } + } else if hasCurrentVideoNode { replaceVideoNode = true } } else { @@ -1352,7 +1383,64 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr }, cancel: { chatMessageWebFileCancelInteractiveFetch(account: context.account, image: image) }) - } else if let file = media as? TelegramMediaFile { + } else if var file = media as? TelegramMediaFile { + var uploading = false + if file.resource is VideoLibraryMediaResource { + uploading = true + } + + if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading { + loadHLSRangeVideoFile = file + + var passFile = true + if NativeVideoContent.isHLSVideo(file: file), let minimizedQualityFile = HLSVideoContent.minimizedHLSQuality(file: .message(message: MessageReference(message), media: file)) { + file = minimizedQualityFile.file.media + if hlsInlinePlaybackRange == nil { + passFile = false + } + } + + if passFile { + updateVideoFile = file + if hasCurrentVideoNode { + if let currentFile = currentMedia as? TelegramMediaFile { + if currentFile.resource is EmptyMediaResource { + replaceVideoNode = true + } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { + replaceVideoNode = true + } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { + replaceVideoNode = true + } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { + replaceVideoNode = true + } + } + } else if !(file.resource is LocalFileVideoMediaResource) { + replaceVideoNode = true + } + } else if hasCurrentVideoNode { + replaceVideoNode = true + } + } else { + if hasCurrentVideoNode { + replaceVideoNode = false + } + + if file.isAnimatedSticker || file.isVideoSticker { + updateAnimatedStickerFile = file + if hasCurrentAnimatedStickerNode { + if let currentMedia = currentMedia { + if !currentMedia.isSemanticallyEqual(to: file) { + replaceAnimatedStickerNode = true + } + } else { + replaceAnimatedStickerNode = true + } + } else { + replaceAnimatedStickerNode = true + } + } + } + if isSecretMedia { updateImageSignal = { synchronousLoad, _ in return chatSecretMessageVideo(account: context.account, userLocation: .peer(message.id.peerId), videoReference: .message(message: MessageReference(message), media: file)) @@ -1378,49 +1466,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } - var uploading = false - if file.resource is VideoLibraryMediaResource { - uploading = true - } - - if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !uploading { - updateVideoFile = file - if hasCurrentVideoNode { - if let currentFile = currentMedia as? TelegramMediaFile { - if currentFile.resource is EmptyMediaResource { - replaceVideoNode = true - } else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId { - replaceVideoNode = true - } else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile { - replaceVideoNode = true - } else if file.isAnimated && currentFile.fileId.namespace == Namespaces.Media.LocalFile && file.fileId.namespace == Namespaces.Media.CloudFile { - replaceVideoNode = true - } - } - } else if !(file.resource is LocalFileVideoMediaResource) { - replaceVideoNode = true - } - } else { - if hasCurrentVideoNode { - replaceVideoNode = false - } - - if file.isAnimatedSticker || file.isVideoSticker { - updateAnimatedStickerFile = file - if hasCurrentAnimatedStickerNode { - if let currentMedia = currentMedia { - if !currentMedia.isSemanticallyEqual(to: file) { - replaceAnimatedStickerNode = true - } - } else { - replaceAnimatedStickerNode = true - } - } else { - replaceAnimatedStickerNode = true - } - } - } - updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { if file.isAnimated { @@ -1489,6 +1534,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } + if !reloadMedia { + updateImageSignal = nil + } else { + print("reload media") + } var isExtendedMedia = false if statusUpdated { @@ -1661,12 +1711,27 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr let loopVideo = updatedVideoFile.isAnimated let videoContent: UniversalVideoContent - 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 + videoContent = NativeVideoContent( + id: .message(message.stableId, updatedVideoFile.fileId), + userLocation: .peer(message.id.peerId), + fileReference: .message(message: MessageReference(message), media: updatedVideoFile), + limitedFileRange: hlsInlinePlaybackRange, + streamVideo: streamVideo ? .conservative : .none, + loopVideo: loopVideo, + enableSound: false, + fetchAutomatically: false, + onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), + autoFetchFullSizeThumbnail: true, + 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 _ = 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 @@ -1685,6 +1750,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr videoNode.isHidden = true strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) } + //videoNode.alpha = 0.5 updatedVideoNodeReadySignal = videoNode.ready updatedPlayerStatusSignal = videoNode.status @@ -1916,6 +1982,36 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.updateStatus(animated: synchronousLoads) strongSelf.pinchContainerNode.isPinchGestureEnabled = !isSecretMedia && !isExtendedMediaPreview && !hasSpoiler + + strongSelf.appliedHlsInlinePlaybackRange = hlsInlinePlaybackRange + + if let loadHLSRangeVideoFile, NativeVideoContent.isHLSVideo(file: loadHLSRangeVideoFile) { + if strongSelf.hlsInlinePlaybackRangeDisposable == nil { + strongSelf.hlsInlinePlaybackRangeDisposable = (HLSVideoContent.minimizedHLSQualityPreloadData( + postbox: context.account.postbox, + file: .message(message: MessageReference(message), media: loadHLSRangeVideoFile), + userLocation: .peer(message.id.peerId), + prefixSeconds: 10, + autofetchPlaylist: false + ) + //|> delay(2.0, queue: .mainQueue()) + |> deliverOnMainQueue).startStrict(next: { [weak strongSelf] preloadData in + guard let strongSelf else { + return + } + let hlsInlinePlaybackRange: Range? + if let preloadData { + hlsInlinePlaybackRange = preloadData.1 + } else { + hlsInlinePlaybackRange = nil + } + if strongSelf.hlsInlinePlaybackRange != hlsInlinePlaybackRange { + strongSelf.hlsInlinePlaybackRange = hlsInlinePlaybackRange + strongSelf.requestInlineUpdate?() + } + }) + } + } } }) }) @@ -2140,6 +2236,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } + + if let file = self.media as? TelegramMediaFile, NativeVideoContent.isHLSVideo(file: file) { + fetchStatus = .Local + } let formatting = DataSizeStringFormatting(strings: strings, decimalSeparator: decimalSeparator) @@ -2185,15 +2285,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if let duration = file.duration, !message.flags.contains(.Unsent) { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : Int32(duration), position: playerPosition) if isMediaStreamable(message: message, media: file) { - if NativeVideoContent.isHLSVideo(file: file) { - mediaDownloadState = .fetching(progress: nil) - badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil) - } else { - badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) - mediaDownloadState = .fetching(progress: automaticPlayback ? nil : adjustedProgress) - if self.playerStatus?.status == .playing { - mediaDownloadState = nil - } + badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) + mediaDownloadState = .fetching(progress: automaticPlayback ? nil : adjustedProgress) + if self.playerStatus?.status == .playing { + mediaDownloadState = nil } state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor) } else { @@ -2292,11 +2387,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr do { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : (file.duration.flatMap { Int32(floor($0)) } ?? 0), position: playerPosition) if wideLayout { - if NativeVideoContent.isHLSVideo(file: file) { - state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor) - mediaDownloadState = nil - badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil) - } else if isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max { + if isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max { state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor) badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(fileSize, formatting: formatting), muted: muted, active: true) mediaDownloadState = .remote @@ -2680,8 +2771,13 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { var isAnimated = false - if let file = self.media as? TelegramMediaFile, file.isAnimated { - isAnimated = true + if let file = self.media as? TelegramMediaFile { + if NativeVideoContent.isHLSVideo(file: file) { + return nil + } + if file.isAnimated { + isAnimated = true + } } var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index d104205259..fd998f651e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -110,7 +110,7 @@ private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String } private func dateHeaderTimestampId(timestamp: Int32) -> Int32 { - if timestamp == scheduleWhenOnlineTimestamp { + if timestamp == scheduleWhenOnlineTimestamp || timestamp >= Int32.max - 1000 { return timestamp } else if timestamp == Int32.max { return timestamp / (granularity) * (granularity) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index fbf52337fa..e6e5362a0c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -89,6 +89,12 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.item?.controllerInteraction.playMessageEffect(message) } + self.interactiveImageNode.requestInlineUpdate = { [weak self] in + guard let self else { + return + } + self.requestInlineUpdate?() + } } required public init?(coder aDecoder: NSCoder) { @@ -560,4 +566,11 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } return nil } + + override public func getStatusNode() -> ASDisplayNode? { + if !self.interactiveImageNode.dateAndStatusNode.isHidden { + return self.interactiveImageNode.dateAndStatusNode + } + return nil + } } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 26551a1cb6..b5e99792de 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -617,6 +617,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -1340,7 +1342,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { case let .localization(identifier): strongSelf.presentController(LanguageLinkPreviewController(context: strongSelf.context, identifier: identifier), .window(.root), nil) case .proxy, .confirmationCode, .cancelAccountReset, .share: - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, openPeer: { peer, _ in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, forceUpdate: false, openPeer: { peer, _ in if let strongSelf = self { strongSelf.openPeer(peer: peer) } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 57f0cf7e99..ce99d5b1f8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -474,6 +474,8 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 3f2527c6e7..fa52fd8c41 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -253,6 +253,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openJoinLink: (String) -> Void public let openWebView: (String, String, Bool, ChatOpenWebViewSource) -> Void public let activateAdAction: (EngineMessage.Id, Promise?, Bool, Bool) -> Void + public let adContextAction: (Message, ASDisplayNode, ContextGesture?) -> Void + public let removeAd: (Data) -> Void public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void public let saveMediaToFiles: (EngineMessage.Id) -> Void public let openNoAdsDemo: () -> Void @@ -384,6 +386,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openJoinLink: @escaping (String) -> Void, openWebView: @escaping (String, String, Bool, ChatOpenWebViewSource) -> Void, activateAdAction: @escaping (EngineMessage.Id, Promise?, Bool, Bool) -> Void, + adContextAction: @escaping (Message, ASDisplayNode, ContextGesture?) -> Void, + removeAd: @escaping (Data) -> Void, openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void, saveMediaToFiles: @escaping (EngineMessage.Id) -> Void, openNoAdsDemo: @escaping () -> Void, @@ -494,6 +498,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openJoinLink = openJoinLink self.openWebView = openWebView self.activateAdAction = activateAdAction + self.adContextAction = adContextAction + self.removeAd = removeAd self.openRequestedPeerSelection = openRequestedPeerSelection self.saveMediaToFiles = saveMediaToFiles self.openNoAdsDemo = openNoAdsDemo diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index a3a42cc62c..512b446dcc 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -1534,6 +1534,8 @@ public extension EmojiPagerContentComponent { displaySearchWithPlaceholder = strings.Common_Search } else if case .stickerAlt = subject { displaySearchWithPlaceholder = strings.Common_Search + } else if case .reactionList = subject { + displaySearchWithPlaceholder = strings.Common_Search } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index fda1f35734..1ed7108d39 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -24,19 +24,34 @@ public final class GiftItemComponent: Component { case red case blue - var colors: [UIColor] { + func colors(theme: PresentationTheme) -> [UIColor] { switch self { case .red: - return [ - UIColor(rgb: 0xed1c26), - UIColor(rgb: 0xff5c55) - - ] + if theme.overallDarkAppearance { + return [ + UIColor(rgb: 0x522124), + UIColor(rgb: 0x653634) + + ] + } else { + return [ + UIColor(rgb: 0xed1c26), + UIColor(rgb: 0xff5c55) + + ] + } case .blue: - return [ - UIColor(rgb: 0x34a4fc), - UIColor(rgb: 0x6fd3ff) - ] + if theme.overallDarkAppearance { + return [ + UIColor(rgb: 0x142e42), + UIColor(rgb: 0x354f5b) + ] + } else { + return [ + UIColor(rgb: 0x34a4fc), + UIColor(rgb: 0x6fd3ff) + ] + } } } } @@ -166,10 +181,15 @@ public final class GiftItemComponent: Component { func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isFirstTime = self.component == nil - + let previousComponent = self.component self.component = component self.componentState = state + var themeUpdated = false + if previousComponent?.theme !== component.theme { + themeUpdated = true + } + let size = CGSize(width: availableSize.width, height: component.title != nil ? 178.0 : 154.0) if component.isLoading { @@ -292,11 +312,9 @@ public final class GiftItemComponent: Component { let buttonColor: UIColor var isStars = false - if component.isSoldOut { - buttonColor = component.theme.list.itemDestructiveColor - } else if component.price.containsEmoji { - buttonColor = UIColor(rgb: 0xd3720a) - isStars = true + if component.price.containsEmoji { + buttonColor = component.theme.overallDarkAppearance ? UIColor(rgb: 0xffc337) : UIColor(rgb: 0xd3720a) + isStars = !component.isSoldOut } else { buttonColor = component.theme.list.itemAccentColor } @@ -341,8 +359,8 @@ public final class GiftItemComponent: Component { } ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize) - if self.ribbon.image == nil { - self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors, direction: .diagonal) + if self.ribbon.image == nil || themeUpdated { + self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: ribbon.color.colors(theme: component.theme), direction: .diagonal) } if let ribbonImage = self.ribbon.image { self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size) @@ -593,7 +611,7 @@ private final class StarsButtonEffectLayer: SimpleLayer { let emitter = CAEmitterCell() emitter.name = "emitter" emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage - emitter.birthRate = 25.0 + emitter.birthRate = 14.0 emitter.lifetime = 2.0 emitter.velocity = 12.0 emitter.velocityRange = 3 diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD index 50f4c91908..854b81c822 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD @@ -42,6 +42,7 @@ swift_library( "//submodules/InAppPurchaseManager", "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/Gifts/GiftSetupScreen", + "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 869d9a61c0..f27b14029e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -25,6 +25,7 @@ import GiftItemComponent import InAppPurchaseManager import TabSelectorComponent import GiftSetupScreen +import GiftViewScreen import UndoUI final class GiftOptionsScreenComponent: Component { @@ -123,6 +124,39 @@ final class GiftOptionsScreenComponent: Component { private let tabSelector = ComponentView() private var starsFilter: StarsFilter = .all + private var _effectiveStarGifts: ([StarGift], StarsFilter)? + private var effectiveStarGifts: [StarGift]? { + get { + if case .all = self.starsFilter { + return self.state?.starGifts + } else { + if let (currentGifts, currentFilter) = self._effectiveStarGifts, currentFilter == self.starsFilter { + return currentGifts + } else if let allGifts = self.state?.starGifts { + let filteredGifts: [StarGift] = allGifts.filter { + switch self.starsFilter { + case .all: + return true + case .limited: + if $0.availability != nil { + return true + } + case let .stars(stars): + if $0.price == stars { + return true + } + } + return false + } + self._effectiveStarGifts = (filteredGifts, self.starsFilter) + return filteredGifts + } else { + return nil + } + } + } + } + private var isUpdating: Bool = false private var starsStateDisposable: Disposable? @@ -242,7 +276,7 @@ final class GiftOptionsScreenComponent: Component { } let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0) - if let starGifts = self.state?.starGifts { + if let starGifts = self.effectiveStarGifts { let sideInset: CGFloat = 16.0 + environment.safeInsets.left let optionSpacing: CGFloat = 10.0 @@ -261,19 +295,6 @@ final class GiftOptionsScreenComponent: Component { } if isVisible { - switch self.starsFilter { - case .all: - break - case .limited: - if gift.availability == nil { - continue - } - case let .stars(stars): - if gift.price != stars { - continue - } - } - let itemId = AnyHashable(gift.id) validIds.append(itemId) @@ -289,7 +310,19 @@ final class GiftOptionsScreenComponent: Component { self.starsItems[itemId] = visibleItem } - let isSoldOut = gift.availability?.remains == 0 + var ribbon: GiftItemComponent.Ribbon? + if let _ = gift.soldOut { + ribbon = GiftItemComponent.Ribbon( + text: environment.strings.Gift_Options_Gift_SoldOut, + color: .red + ) + } else if let _ = gift.availability { + ribbon = GiftItemComponent.Ribbon( + text: environment.strings.Gift_Options_Gift_Limited, + color: .blue + ) + } + let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -300,14 +333,9 @@ final class GiftOptionsScreenComponent: Component { theme: environment.theme, peer: nil, subject: .starGift(gift.id, gift.file), - price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)", - ribbon: gift.availability != nil ? - GiftItemComponent.Ribbon( - text: environment.strings.Gift_Options_Gift_Limited, - color: .blue - ) - : nil, - isSoldOut: isSoldOut + price: "⭐️ \(gift.price)", + ribbon: ribbon, + isSoldOut: gift.soldOut != nil ) ), effectAlignment: .center, @@ -321,16 +349,11 @@ final class GiftOptionsScreenComponent: Component { mainController = controller } if gift.availability?.remains == 0 { - self.dismissAllTooltips(controller: mainController) - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let resultController = UndoOverlayController( - presentationData: presentationData, - content: .sticker(context: component.context, file: gift.file, loop: false, title: nil, text: presentationData.strings.Gift_Options_SoldOut_Text, undoText: nil, customAction: nil), - elevatedLayout: false, - action: { _ in return true } + let giftController = GiftViewScreen( + context: component.context, + subject: .soldOutGift(gift) ) - mainController.present(resultController, in: .window(.root)) - HapticFeedback().error() + mainController.push(giftController) } else { let giftController = GiftSetupScreen( context: component.context, @@ -340,6 +363,7 @@ final class GiftOptionsScreenComponent: Component { ) mainController.push(giftController) } + } } }, @@ -601,7 +625,8 @@ final class GiftOptionsScreenComponent: Component { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, - highlightColor: accentColor.withAlphaComponent(0.2), + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -789,7 +814,8 @@ final class GiftOptionsScreenComponent: Component { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, - highlightColor: accentColor.withAlphaComponent(0.2), + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -892,9 +918,9 @@ final class GiftOptionsScreenComponent: Component { contentHeight += tabSelectorSize.height contentHeight += 19.0 - if let starGifts = state.starGifts { + if let starGifts = self.effectiveStarGifts { self.starsItemsOrigin = contentHeight - + let starsOptionSize = CGSize(width: optionWidth, height: 154.0) contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * starsOptionSize.height contentHeight += 66.0 diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index e503f27700..465ffb2c88 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -417,6 +417,33 @@ final class GiftSetupScreenComponent: Component { } } + @objc private func previewTap() { + func hasFirstResponder(_ view: UIView) -> Bool { + if view.isFirstResponder { + return true + } + for subview in view.subviews { + if hasFirstResponder(subview) { + return true + } + } + return false + } + + self.currentInputMode = .keyboard + if hasFirstResponder(self) { + if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { + if titleView.isActive { + titleView.deactivateInput() + } else { + self.endEditing(true) + } + } + } else { + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -693,28 +720,7 @@ final class GiftSetupScreenComponent: Component { )))) self.resetText = nil - - var inputHeight: CGFloat = 0.0 - inputHeight += self.updateInputMediaNode( - component: component, - availableSize: availableSize, - bottomInset: environment.safeInsets.bottom, - inputHeight: 0.0, - effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), - metrics: environment.metrics, - deviceMetrics: environment.deviceMetrics, - transition: transition - ) - if self.inputMediaNode == nil { - if environment.inputHeight.isZero && self.textInputState.isEditing, let previousInputHeight = self.previousInputHeight { - inputHeight = previousInputHeight - } else { - inputHeight = environment.inputHeight - } - } - let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" - let introFooter: AnyComponent? switch component.subject { case .premium: @@ -751,7 +757,26 @@ final class GiftSetupScreenComponent: Component { } contentHeight += introSectionSize.height contentHeight += sectionSpacing - + + var inputHeight: CGFloat = 0.0 + inputHeight += self.updateInputMediaNode( + component: component, + availableSize: availableSize, + bottomInset: environment.safeInsets.bottom, + inputHeight: 0.0, + effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false), + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + transition: transition + ) + if self.inputMediaNode == nil { + if environment.inputHeight.isZero && self.textInputState.isEditing, let previousInputHeight = self.previousInputHeight { + inputHeight = previousInputHeight + } else { + inputHeight = environment.inputHeight + } + } + 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 @@ -793,6 +818,8 @@ final class GiftSetupScreenComponent: Component { if introContentView.superview == nil { if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) { placeholderView.addSubview(introContentView) + + placeholderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap))) } } transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 6872f69960..6ac2b53c30 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -35,6 +35,8 @@ private final class GiftViewSheetContent: CombinedComponent { let updateSavedToProfile: (Bool) -> Void let convertToStars: () -> Void let openStarsIntro: () -> Void + let sendGift: (EnginePeer.Id) -> Void + let openMyGifts: () -> Void init( context: AccountContext, @@ -43,7 +45,9 @@ private final class GiftViewSheetContent: CombinedComponent { openPeer: @escaping (EnginePeer) -> Void, updateSavedToProfile: @escaping (Bool) -> Void, convertToStars: @escaping () -> Void, - openStarsIntro: @escaping () -> Void + openStarsIntro: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void, + openMyGifts: @escaping () -> Void ) { self.context = context self.subject = subject @@ -52,6 +56,8 @@ private final class GiftViewSheetContent: CombinedComponent { self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro + self.sendGift = sendGift + self.openMyGifts = openMyGifts } static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool { @@ -74,6 +80,7 @@ private final class GiftViewSheetContent: CombinedComponent { var cachedCloseImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? + var cachedSmallChevronImage: (UIImage, PresentationTheme)? var inProgress = false @@ -84,17 +91,16 @@ private final class GiftViewSheetContent: CombinedComponent { if let arguments = subject.arguments { var peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId] - if let fromPeerId = arguments.fromPeerId { + if let fromPeerId = arguments.fromPeerId, !peerIds.contains(fromPeerId) { peerIds.append(fromPeerId) } - self.disposable = combineLatest(queue: Queue.mainQueue(), context.engine.data.get(EngineDataMap( peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) } )), - context.engine.payments.cachedStarGifts() + .single(nil) |> then(context.engine.payments.cachedStarGifts()) ).startStrict(next: { [weak self] peers, starGifts in if let strongSelf = self { var peersMap: [EnginePeer.Id: EnginePeer] = [:] @@ -134,12 +140,11 @@ private final class GiftViewSheetContent: CombinedComponent { let closeButton = Child(Button.self) let animation = Child(GiftAnimationComponent.self) let title = Child(MultilineTextComponent.self) - let amount = Child(BalancedTextComponent.self) - let amountStar = Child(BundleIconComponent.self) let description = Child(MultilineTextComponent.self) + let hiddenText = Child(MultilineTextComponent.self) let table = Child(TableComponent.self) + let additionalText = Child(MultilineTextComponent.self) let button = Child(SolidRoundedButtonComponent.self) - let secondaryButton = Child(SolidRoundedButtonComponent.self) let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) @@ -154,8 +159,7 @@ private final class GiftViewSheetContent: CombinedComponent { let state = context.state let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let textSideInset: CGFloat = 32.0 + environment.safeInsets.left - + let closeImage: UIImage if let (image, theme) = state.cachedCloseImage, theme === environment.theme { closeImage = image @@ -175,35 +179,43 @@ private final class GiftViewSheetContent: CombinedComponent { transition: .immediate ) + let titleString: String let animationFile: TelegramMediaFile? let stars: Int64 let convertStars: Int64 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 { + var date: Int32? + var soldOut = false + var nameHidden = false + if case let .soldOutGift(gift) = component.subject { + animationFile = gift.file + stars = gift.price + text = nil + entities = nil + limitTotal = gift.availability?.total + convertStars = 0 + soldOut = true + titleString = strings.Gift_View_UnavailableTitle + } else if let arguments = component.subject.arguments { animationFile = arguments.gift.file stars = arguments.gift.price text = arguments.text 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 + titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title + nameHidden = arguments.nameHidden } else { animationFile = nil stars = 0 @@ -211,10 +223,13 @@ private final class GiftViewSheetContent: CombinedComponent { entities = nil limitTotal = nil convertStars = 0 + titleString = "" } var descriptionText: String - if incoming { + if soldOut { + descriptionText = strings.Gift_View_UnavailableDescription + } else if incoming { if !converted { descriptionText = strings.Gift_View_KeepOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string } else { @@ -242,19 +257,11 @@ private final class GiftViewSheetContent: CombinedComponent { } descriptionText = modifiedString } - - var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) - if outgoing { - formattedAmount = "- \(formattedAmount)" - } - let countFont: UIFont = Font.semibold(17.0) - let amountText = formattedAmount - let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor - + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( - string: incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title, + string: titleString, font: Font.bold(25.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center @@ -265,27 +272,100 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - - let amountAttributedText = NSMutableAttributedString(string: amountText, font: countFont, textColor: countColor) - let amount = amount.update( - component: BalancedTextComponent( - text: .plain(amountAttributedText), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.2 - ), - availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), - transition: .immediate + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0)) ) - let amountStar = amountStar.update( - component: BundleIconComponent( - name: "Premium/Stars/StarMedium", - tintColor: nil - ), - availableSize: context.availableSize, - transition: .immediate - ) + var originY: CGFloat = 0.0 + if let animationFile { + let animation = animation.update( + component: GiftAnimationComponent( + context: component.context, + theme: environment.theme, + file: animationFile + ), + availableSize: CGSize(width: 128.0, height: 128.0), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 25.0)) + ) + originY += animation.size.height + } + originY += 80.0 + if soldOut { + originY -= 12.0 + } + + let linkColor = theme.actionSheet.controlAccentColor + if !descriptionText.isEmpty { + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) + } + + let textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0) + let textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } + let description = description.update( + component: MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.openStarsIntro() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0)) + ) + originY += description.size.height + 21.0 + if soldOut { + originY -= 7.0 + } + } else { + originY += 21.0 + } + + if nameHidden { + let textFont = Font.regular(13.0) + let textColor = theme.list.itemSecondaryTextColor + + let hiddenText = hiddenText.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: text != nil ? strings.Gift_View_NameAndMessageHidden : strings.Gift_View_NameHidden, font: textFont, textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 2, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(hiddenText + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) + ) + + originY += hiddenText.size.height + originY += 11.0 + } let tableFont = Font.regular(15.0) let tableBoldFont = Font.semibold(15.0) @@ -296,13 +376,52 @@ private final class GiftViewSheetContent: CombinedComponent { let tableTextColor = theme.list.itemPrimaryTextColor let tableLinkColor = theme.list.itemAccentColor var tableItems: [TableComponent.Item] = [] - - if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { - tableItems.append(.init( - id: "from", - title: strings.Gift_View_From, - component: AnyComponent( - Button( + + if !soldOut { + if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] { + let fromComponent: AnyComponent + if incoming { + fromComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: peer + ) + ), + action: { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Send, + color: theme.list.itemAccentColor + )), + action: { + component.sendGift(peerId) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + )) + ) + ], spacing: 4.0) + ) + } else { + fromComponent = AnyComponent(Button( content: AnyComponent( PeerCellComponent( context: component.context, @@ -312,37 +431,114 @@ private final class GiftViewSheetContent: CombinedComponent { ) ), action: { - if "".isEmpty { - component.openPeer(peer) - Queue.mainQueue().after(1.0, { - component.cancel(false) - }) - } + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) } + )) + } + tableItems.append(.init( + id: "from", + title: strings.Gift_View_From, + component: fromComponent + )) + } else { + tableItems.append(.init( + id: "from_anon", + title: strings.Gift_View_From, + component: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil + ) ) + )) + } + } + + if case let .soldOutGift(gift) = component.subject, let soldOut = gift.soldOut { + tableItems.append(.init( + id: "firstDate", + title: strings.Gift_View_FirstSale, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.firstSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) - } else { + tableItems.append(.init( - id: "from_anon", - title: strings.Gift_View_From, + id: "lastDate", + title: strings.Gift_View_LastSale, component: AnyComponent( - PeerCellComponent( - context: component.context, - theme: theme, - strings: strings, - peer: nil - ) + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.lastSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + } else if let date { + tableItems.append(.init( + id: "date", + title: strings.Gift_View_Date, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) } - - tableItems.append(.init( - id: "date", - title: strings.Gift_View_Date, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + + let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator))" + let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor) + let range = (valueAttributedString.string as NSString).range(of: "⭐️") + if range.location != NSNotFound { + valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range) + valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range) + } + + let valueComponent: AnyComponent + if incoming && !converted { + valueComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + )) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Sale(strings.Gift_View_Sale_Stars(Int32(convertStars))).string, + color: theme.list.itemAccentColor + )), + action: { + component.convertToStars() + } + )) + ) + ], spacing: 4.0) ) + } else { + valueComponent = AnyComponent(MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: theme.list.mediaPlaceholderColor, + text: .plain(valueAttributedString), + maximumNumberOfLines: 0 + )) + } + + tableItems.append(.init( + id: "value", + title: strings.Gift_View_Value, + component: valueComponent, + insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) )) if let limitTotal { @@ -356,10 +552,39 @@ private final class GiftViewSheetContent: CombinedComponent { id: "availability", title: strings.Gift_View_Availability, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_NewOf("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) ) )) } + + if savedToProfile { + tableItems.append(.init( + id: "visibility", + title: strings.Gift_View_Visibility, + component: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Visibility_Visible, font: tableFont, textColor: tableTextColor)))) + ), + AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: component.context, + text: strings.Gift_View_Visibility_Hide, + color: theme.list.itemAccentColor + )), + action: { + component.updateSavedToProfile(false) + } + )) + ) + ], spacing: 4.0) + ), + insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) + )) + } if let text { let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil) @@ -389,53 +614,36 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), transition: .immediate ) - - let textFont = Font.regular(15.0) - let linkColor = theme.actionSheet.controlAccentColor - - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0)) + context.add(table + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) ) - - var originY: CGFloat = 0.0 - if let animationFile { - let animation = animation.update( - component: GiftAnimationComponent( - context: component.context, - theme: environment.theme, - file: animationFile - ), - availableSize: CGSize(width: 128.0, height: 128.0), - transition: .immediate - ) - context.add(animation - .position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 25.0)) - ) - originY += animation.size.height - } - originY += 69.0 - - var descriptionSize: CGSize = .zero - if !descriptionText.isEmpty { - if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { - state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) + originY += table.size.height + 23.0 + + if incoming && !converted { + if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { + state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } + let descriptionText = savedToProfile ? strings.Gift_View_DisplayedInfo : strings.Gift_View_HiddenInfo - let textColor = theme.list.itemPrimaryTextColor + let textFont = Font.regular(13.0) + let textColor = theme.list.itemSecondaryTextColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString - if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) } - let description = description.update( + + originY -= 5.0 + let additionalText = additionalText.update( component: MultilineTextComponent( text: .plain(attributedString), horizontalAlignment: .center, maximumNumberOfLines: 5, lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.2), + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -444,64 +652,23 @@ private final class GiftViewSheetContent: CombinedComponent { } }, tapAction: { _, _ in - component.openStarsIntro() + component.openMyGifts() + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) - descriptionSize = description.size - var descriptionOrigin = originY - if "".isEmpty { - descriptionOrigin += amount.size.height + 13.0 - } - context.add(description - .position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0)) + context.add(additionalText + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0)) ) - originY += description.size.height + 10.0 - } else { - originY += 11.0 + originY += additionalText.size.height + originY += 16.0 } - let amountSpacing: CGFloat = 1.0 - let totalAmountWidth: CGFloat = amount.size.width + amountSpacing + amountStar.size.width - let amountOriginX: CGFloat = floor(context.availableSize.width - totalAmountWidth) / 2.0 - - var amountOrigin = originY - if "".isEmpty { - amountOrigin -= descriptionSize.height + 10.0 - if descriptionSize.height > 0 { - originY += amount.size.height + 26.0 - } else { - originY += amount.size.height + 2.0 - } - } else { - originY += amount.size.height + 20.0 - } - - let amountLabelOriginX: CGFloat - let amountStarOriginX: CGFloat - if !"".isEmpty { - amountStarOriginX = amountOriginX + amountStar.size.width / 2.0 - amountLabelOriginX = amountOriginX + amountStar.size.width + amountSpacing + amount.size.width / 2.0 - } else { - amountLabelOriginX = amountOriginX + amount.size.width / 2.0 - amountStarOriginX = amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0 - } - - context.add(amount - .position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0)) - ) - context.add(amountStar - .position(CGPoint(x: amountStarOriginX, y: amountOrigin + amountStar.size.height / 2.0 - UIScreenPixel)) - ) - - context.add(table - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) - ) - originY += table.size.height + 23.0 - - if incoming && !converted { + if incoming && !converted && !savedToProfile { let button = button.update( component: SolidRoundedButtonComponent( title: savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display, @@ -528,32 +695,6 @@ private final class GiftViewSheetContent: CombinedComponent { ) originY += button.size.height originY += 7.0 - - let secondaryButton = secondaryButton.update( - component: SolidRoundedButtonComponent( - title: strings.Gift_View_Convert(strings.Gift_View_Convert_Stars(Int32(convertStars))).string, - theme: SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: linkColor), - font: .regular, - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, - gloss: false, - iconName: nil, - animationName: nil, - iconPosition: .left, - isLoading: false, - action: { - component.convertToStars() - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), - transition: context.transition - ) - let secondaryButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: secondaryButton.size) - context.add(secondaryButton - .position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY)) - ) - originY += secondaryButton.size.height } else { let button = button.update( component: SolidRoundedButtonComponent( @@ -603,6 +744,8 @@ private final class GiftViewSheetComponent: CombinedComponent { let updateSavedToProfile: (Bool) -> Void let convertToStars: () -> Void let openStarsIntro: () -> Void + let sendGift: (EnginePeer.Id) -> Void + let openMyGifts: () -> Void init( context: AccountContext, @@ -610,7 +753,9 @@ private final class GiftViewSheetComponent: CombinedComponent { openPeer: @escaping (EnginePeer) -> Void, updateSavedToProfile: @escaping (Bool) -> Void, convertToStars: @escaping () -> Void, - openStarsIntro: @escaping () -> Void + openStarsIntro: @escaping () -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void, + openMyGifts: @escaping () -> Void ) { self.context = context self.subject = subject @@ -618,6 +763,8 @@ private final class GiftViewSheetComponent: CombinedComponent { self.updateSavedToProfile = updateSavedToProfile self.convertToStars = convertToStars self.openStarsIntro = openStarsIntro + self.sendGift = sendGift + self.openMyGifts = openMyGifts } static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool { @@ -660,7 +807,9 @@ private final class GiftViewSheetComponent: CombinedComponent { openPeer: context.component.openPeer, updateSavedToProfile: context.component.updateSavedToProfile, convertToStars: context.component.convertToStars, - openStarsIntro: context.component.openStarsIntro + openStarsIntro: context.component.openStarsIntro, + sendGift: context.component.sendGift, + openMyGifts: context.component.openMyGifts )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, @@ -730,6 +879,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case message(EngineMessage) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) + case soldOutGift(StarGift) 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 { @@ -739,6 +889,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { } case let .profileGift(peerId, gift): 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) + case .soldOutGift: + return nil } return nil } @@ -762,6 +914,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { var updateSavedToProfileImpl: ((Bool) -> Void)? var convertToStarsImpl: (() -> Void)? var openStarsIntroImpl: (() -> Void)? + var sendGiftImpl: ((EnginePeer.Id) -> Void)? + var openMyGiftsImpl: (() -> Void)? super.init( context: context, @@ -779,6 +933,12 @@ public class GiftViewScreen: ViewControllerComponentContainer { }, openStarsIntro: { openStarsIntroImpl?() + }, + sendGift: { peerId in + sendGiftImpl?(peerId) + }, + openMyGifts: { + openMyGiftsImpl?() } ), navigationBarAppearance: .none, @@ -820,20 +980,16 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.dismissAnimated() - let title: String = added ? presentationData.strings.Gift_Displayed_Title : presentationData.strings.Gift_Hidden_Title - var text = added ? presentationData.strings.Gift_Displayed_Text : presentationData.strings.Gift_Hidden_Text - if let _ = updateSavedToProfile { - text = text.replacingOccurrences(of: "]()", with: "").replacingOccurrences(of: "[", with: "") - } - if let navigationController { + let text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText + if let navigationController = self.navigationController as? NavigationController { Queue.mainQueue().after(0.5) { if let lastController = navigationController.viewControllers.last as? ViewController { let resultController = UndoOverlayController( presentationData: presentationData, - content: .sticker(context: context, file: arguments.gift.file, loop: false, title: title, text: text, undoText: nil, customAction: nil), + content: .sticker(context: context, file: arguments.gift.file, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil), elevatedLayout: lastController is ChatController, action: { [weak navigationController] action in - if case .info = action, let navigationController { + if case .undo = action, let navigationController { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { [weak navigationController] peer in guard let peer, let navigationController else { @@ -866,51 +1022,87 @@ public class GiftViewScreen: ViewControllerComponentContainer { return } - let controller = textAlertController( - context: self.context, - title: presentationData.strings.Gift_Convert_Title, - text: presentationData.strings.Gift_Convert_Text(fromPeerName, presentationData.strings.Gift_Convert_Stars(Int32(arguments.convertStars))).string, - actions: [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak navigationController] in - if let convertToStars { - convertToStars() - } else { - let _ = (context.engine.payments.convertStarGift(messageId: messageId) - |> deliverOnMainQueue).startStandalone() - } - self?.dismissAnimated() - - if let navigationController { - Queue.mainQueue().after(0.5) { - if let starsContext = context.starsContext { - navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true) - } - - if let lastController = navigationController.viewControllers.last as? ViewController { - let resultController = UndoOverlayController( - presentationData: presentationData, - content: .universal( - animation: "StarsBuy", - scale: 0.066, - colors: [:], - title: presentationData.strings.Gift_Convert_Success_Title, - text: presentationData.strings.Gift_Convert_Success_Text(presentationData.strings.Gift_Convert_Success_Text_Stars(Int32(arguments.convertStars))).string, - customUndoText: nil, - timeout: nil - ), - elevatedLayout: lastController is ChatController, - action: { _ in return true} - ) - lastController.present(resultController, in: .window(.root)) + let configuration = GiftConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + if currentTime > starsConvertMaxDate { + //TODO:localize + + let duration: String + if starsConvertMaxDate == 300 { + duration = "5 minutes" + } else { + duration = "90 days" + } + + let controller = textAlertController( + context: self.context, + title: presentationData.strings.Gift_Convert_Title, + text: "Sorry, you can't convert this gift.\n\nStars can only be claimed within \(duration) after receiving a gift.", + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ], + parseMarkdown: true + ) + self.present(controller, in: .window(.root)) + } else { + let delta = starsConvertMaxDate - currentTime + let duration: String + if starsConvertMaxDate == 300 { + duration = "\(delta / 60) minutes" + } else { + duration = "\(delta / 86400) days" + } + //TODO:localize + let text = "Do you want to convert this gift from **\(fromPeerName)** to **\(presentationData.strings.Gift_Convert_Stars(Int32(arguments.convertStars)))**?\n\nConversion is available for the next \(duration).\n\nThis will permanently destroy the gift." + let controller = textAlertController( + context: self.context, + title: presentationData.strings.Gift_Convert_Title, + text: text, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak navigationController] in + if let convertToStars { + convertToStars() + } else { + let _ = (context.engine.payments.convertStarGift(messageId: messageId) + |> deliverOnMainQueue).startStandalone() + } + self?.dismissAnimated() + + if let navigationController { + Queue.mainQueue().after(0.5) { + if let starsContext = context.starsContext { + navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true) + } + + if let lastController = navigationController.viewControllers.last as? ViewController { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsBuy", + scale: 0.066, + colors: [:], + title: presentationData.strings.Gift_Convert_Success_Title, + text: presentationData.strings.Gift_Convert_Success_Text(presentationData.strings.Gift_Convert_Success_Text_Stars(Int32(arguments.convertStars))).string, + customUndoText: nil, + timeout: nil + ), + elevatedLayout: lastController is ChatController, + action: { _ in return true} + ) + lastController.present(resultController, in: .window(.root)) + } } } - } - }) - ], - parseMarkdown: true - ) - self.present(controller, in: .window(.root)) + }) + ], + parseMarkdown: true + ) + self.present(controller, in: .window(.root)) + } } openStarsIntroImpl = { [weak self] in guard let self else { @@ -919,6 +1111,40 @@ public class GiftViewScreen: ViewControllerComponentContainer { let introController = context.sharedContext.makeStarsIntroScreen(context: context) self.push(introController) } + sendGiftImpl = { [weak self] peerId in + guard let self else { + return + } + let _ = (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 = context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions) + self.push(controller) + }) + } + openMyGiftsImpl = { [weak self] in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer, let navigationController else { + return + } + if let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfileGifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController.pushViewController(controller, animated: true) + } + }) + } } required public init(coder aDecoder: NSCoder) { @@ -1332,3 +1558,122 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: context.strokePath() }) } + +private final class ButtonContentComponent: Component { + let context: AccountContext + let text: String + let color: UIColor + + public init( + context: AccountContext, + text: String, + color: UIColor + ) { + self.context = context + self.text = text + self.color = color + } + + public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: UIView { + private var component: ButtonContentComponent? + private weak var componentState: EmptyComponentState? + + private let backgroundLayer = SimpleLayer() + private let title = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.backgroundLayer) + self.backgroundLayer.masksToBounds = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let attributedText = NSAttributedString(string: component.text, font: Font.regular(11.0), textColor: component.color) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white, + text: .plain(attributedText) + ) + ), + environment: {}, + containerSize: availableSize + ) + + let padding: CGFloat = 6.0 + let size = CGSize(width: titleSize.width + padding * 2.0, height: 18.0) + + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let backgroundColor = component.color.withAlphaComponent(0.1) + self.backgroundLayer.backgroundColor = backgroundColor.cgColor + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) + self.backgroundLayer.cornerRadius = size.height / 2.0 + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private struct GiftConfiguration { + static var defaultValue: GiftConfiguration { + return GiftConfiguration(convertToStarsPeriod: 90 * 86400) + } + + let convertToStarsPeriod: Int32 + + fileprivate init(convertToStarsPeriod: Int32) { + self.convertToStarsPeriod = convertToStarsPeriod + } + + static func with(appConfiguration: AppConfiguration) -> GiftConfiguration { + if let data = appConfiguration.data { + var convertToStarsPeriod: Int32? + if let value = data["stargifts_convert_period_max"] as? Double { + convertToStarsPeriod = Int32(value) + } + return GiftConfiguration(convertToStarsPeriod: convertToStarsPeriod ?? GiftConfiguration.defaultValue.convertToStarsPeriod) + } else { + return .defaultValue + } + } +} diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 70f131dbfb..abea4059e8 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -208,7 +208,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { style: .media, placeholder: .plain(presentationData.strings.MediaPicker_AddCaption), maxLength: Int(self.context.userLimits.maxCaptionLength), - queryTypes: [.mention], + queryTypes: [.mention, .hashtag], alwaysDarkWhenHasText: false, resetInputContents: resetInputContents, nextInputMode: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 7623336e54..415ca72422 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -227,12 +227,26 @@ public final class ListMultilineTextFieldItemComponent: Component { return false } + public var isActive: Bool { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + return textFieldView.isActive + } else { + return false + } + } + public func activateInput() { if let textFieldView = self.textField.view as? TextFieldComponent.View { textFieldView.activateInput() } } + public func deactivateInput() { + if let textFieldView = self.textField.view as? TextFieldComponent.View { + textFieldView.deactivateInput() + } + } + public func insertText(text: NSAttributedString) { if let textFieldView = self.textField.view as? TextFieldComponent.View { textFieldView.insertText(text) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index f190635001..482d271297 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -119,7 +119,7 @@ public final class DrawingMessageRenderer { let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) let _ = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) - Queue.mainQueue().after(0.2, { + Queue.mainQueue().after(0.35, { var mediaRect: CGRect? if let messageNode = self.messageNodes?.first { if self.isOverlay { @@ -343,7 +343,7 @@ public final class DrawingMessageRenderer { } public func render(completion: @escaping (Result) -> Void) { - Queue.mainQueue().after(0.12) { + Queue.mainQueue().justDispatch { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let defaultPresentationData = defaultPresentationData() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 09b076bbd2..4b7755eda9 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -628,10 +628,20 @@ public final class MediaEditor { case let .asset(asset): textureSource = Signal { subscriber in let isVideo = asset.mediaType == .video - + let targetSize = isVideo ? CGSize(width: 128.0, height: 128.0) : CGSize(width: 1920.0, height: 1920.0) let options = PHImageRequestOptions() - options.deliveryMode = isVideo ? .fastFormat : .highQualityFormat + let deliveryMode: PHImageRequestOptionsDeliveryMode + if isVideo { + if #available(iOS 14.0, *), PHPhotoLibrary.authorizationStatus(for: .readWrite) == .limited { + deliveryMode = .highQualityFormat + } else { + deliveryMode = .fastFormat + } + } else { + deliveryMode = .highQualityFormat + } + options.deliveryMode = deliveryMode options.isNetworkAccessAllowed = true let requestId = PHImageManager.default().requestImage( diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index bc9ec74e11..159e256163 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1229,7 +1229,7 @@ final class MediaEditorScreenComponent: Component { style: .editor, placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption), maxLength: Int(component.context.userLimits.maxStoryCaptionLength), - queryTypes: [.mention], + queryTypes: [.mention, .hashtag], alwaysDarkWhenHasText: false, resetInputContents: nil, nextInputMode: { _ in return nextInputMode }, @@ -1363,12 +1363,12 @@ final class MediaEditorScreenComponent: Component { header: header, isChannel: false, storyItem: nil, - chatLocation: nil + chatLocation: controller.customTarget.flatMap { .peer(id: $0) } )), environment: {}, containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) ) - + if self.inputPanelExternalState.isEditing && controller.node.entitiesView.hasSelection { Queue.mainQueue().justDispatch { controller.node.entitiesView.selectEntity(nil) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift index 634c62295c..956f088e41 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/ContextResultPanelComponent.swift @@ -11,14 +11,18 @@ import PeerListItemComponent final class ContextResultPanelComponent: Component { enum Results: Equatable { case mentions([EnginePeer]) - case hashtags([String]) + case hashtags(EnginePeer?, [String], String) var count: Int { switch self { - case let .hashtags(hashtags): - return hashtags.count case let .mentions(peers): return peers.count + case let .hashtags(peer, hashtags, query): + var count = hashtags.count + if let _ = peer, query.count >= 4 { + count += 2 + } + return count } } } @@ -186,38 +190,23 @@ final class ContextResultPanelComponent: Component { let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0) -// var synchronousLoad = false -// if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) { -// synchronousLoad = hint.synchronousLoad -// } - var validIds: [AnyHashable] = [] - if let range = itemLayout.visibleItems(for: visibleBounds), case let .mentions(peers) = component.results { + if let range = itemLayout.visibleItems(for: visibleBounds) { for index in range.lowerBound ..< range.upperBound { - guard index < peers.count else { + guard index < component.results.count else { continue } let itemFrame = itemLayout.itemFrame(for: index) - var itemTransition = transition - let peer = peers[index] - validIds.append(peer.id) + let id: AnyHashable - let visibleItem: ComponentView - if let current = self.visibleItems[peer.id] { - visibleItem = current - } else { - if !transition.animation.isImmediate { - itemTransition = .immediate - } - visibleItem = ComponentView() - self.visibleItems[peer.id] = visibleItem - } - - let _ = visibleItem.update( - transition: itemTransition, - component: AnyComponent(PeerListItemComponent( + let itemComponent: AnyComponent + switch component.results { + case let .mentions(peers): + let peer = peers[index] + id = peer.id + itemComponent = AnyComponent(PeerListItemComponent( context: component.context, theme: component.theme, strings: component.strings, @@ -236,21 +225,100 @@ final class ContextResultPanelComponent: Component { } component.action(.mention(peer)) } - )), + )) + case let .hashtags(peer, hashtags, query): + var hashtagIndex = index + if let _ = peer, query.count >= 4 { + hashtagIndex -= 2 + } + + if let peer, let addressName = peer.addressName, hashtagIndex < 0 { + //TODO: localize + var isGroup = false + if case let .channel(channel) = peer, case .group = channel.info { + isGroup = true + } + id = hashtagIndex + if hashtagIndex == -2 { + itemComponent = AnyComponent(HashtagListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: nil, + title: "Use #\(query)", + subtitle: "searches posts from all channels", + hashtag: query, + hasNext: index != hashtags.count - 1, + action: { [weak self] hashtag, _ in + guard let self, let component = self.component else { + return + } + component.action(.hashtag(query)) + } + )) + } else { + itemComponent = AnyComponent(HashtagListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: peer, + title: "Use #\(query)@\(addressName)", + subtitle: isGroup ? "searches only posts from this group" : "searches only posts from this channel", + hashtag: "\(query)@\(addressName)", + hasNext: index != hashtags.count - 1, + action: { [weak self] hashtag, _ in + guard let self, let component = self.component else { + return + } + component.action(.hashtag("\(query)@\(addressName)")) + } + )) + } + } else { + let hashtag = hashtags[hashtagIndex] + id = hashtag + itemComponent = AnyComponent(HashtagListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: nil, + title: "#\(hashtag)", + subtitle: nil, + hashtag: hashtag, + hasNext: index != hashtags.count - 1, + action: { [weak self] hashtag, _ in + guard let self, let component = self.component else { + return + } + component.action(.hashtag(hashtag)) + } + )) + } + } + validIds.append(id) + + let visibleItem: ComponentView + if let current = self.visibleItems[id] { + visibleItem = current + } else { + if !transition.animation.isImmediate { + itemTransition = .immediate + } + visibleItem = ComponentView() + self.visibleItems[id] = visibleItem + } + + let _ = visibleItem.update( + transition: itemTransition, + component: itemComponent, environment: {}, containerSize: itemFrame.size ) if let itemView = visibleItem.view { -// var animateIn = false if itemView.superview == nil { -// animateIn = true self.scrollView.addSubview(itemView) } itemTransition.setFrame(view: itemView, frame: itemFrame) - -// if animateIn { -// itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) -// } } } } @@ -284,9 +352,10 @@ final class ContextResultPanelComponent: Component { let sideInset: CGFloat = 3.0 self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition) - let measureItemSize = self.measureItem.update( - transition: .immediate, - component: AnyComponent(PeerListItemComponent( + let itemComponent: AnyComponent + switch component.results { + case .mentions: + itemComponent = AnyComponent(PeerListItemComponent( context: component.context, theme: component.theme, strings: component.strings, @@ -301,7 +370,25 @@ final class ContextResultPanelComponent: Component { hasNext: true, action: { _, _, _ in } - )), + )) + case .hashtags: + itemComponent = AnyComponent(HashtagListItemComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + peer: nil, + title: "AAAAAAAAAAAA", + subtitle: nil, + hashtag: "", + hasNext: true, + action: { _, _ in + } + )) + } + + let measureItemSize = self.measureItem.update( + transition: .immediate, + component: itemComponent, environment: {}, containerSize: CGSize(width: availableSize.width, height: 1000.0) ) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift new file mode 100644 index 0000000000..ed641defc1 --- /dev/null +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift @@ -0,0 +1,509 @@ +import Foundation +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import AccountContext +import TelegramCore +import Postbox +import MultilineTextComponent +import AvatarNode +import TelegramPresentationData +import CheckNode +import TelegramStringFormatting +import AppBundle +import PeerPresenceStatusManager +import EmojiStatusComponent +import ContextUI +import EmojiTextAttachmentView +import TextFormat +import PhotoResources +import ListSectionComponent +import ListItemSwipeOptionContainer + +private let avatarFont = avatarPlaceholderFont(size: 15.0) + +public final class HashtagListItemComponent: Component { + public final class TransitionHint { + public let synchronousLoad: Bool + + public init(synchronousLoad: Bool) { + self.synchronousLoad = synchronousLoad + } + } + + public final class InlineAction: Equatable { + public enum Color: Equatable { + case destructive + } + + public let id: AnyHashable + public let title: String + public let color: Color + public let action: () -> Void + + public init(id: AnyHashable, title: String, color: Color, action: @escaping () -> Void) { + self.id = id + self.title = title + self.color = color + self.action = action + } + + public static func ==(lhs: InlineAction, rhs: InlineAction) -> Bool { + if lhs === rhs { + return true + } + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + } + + public final class InlineActionsState: Equatable { + public let actions: [InlineAction] + + public init(actions: [InlineAction]) { + self.actions = actions + } + + public static func ==(lhs: InlineActionsState, rhs: InlineActionsState) -> Bool { + if lhs === rhs { + return true + } + if lhs.actions != rhs.actions { + return false + } + return true + } + } + + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer? + let title: String + let subtitle: String? + let hashtag: String + let hasNext: Bool + let action: (String, HashtagListItemComponent.View) -> Void + let contextAction: ((String, ContextExtractedContentContainingView, ContextGesture) -> Void)? + let inlineActions: InlineActionsState? + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer?, + title: String, + subtitle: String?, + hashtag: String, + hasNext: Bool, + action: @escaping (String, HashtagListItemComponent.View) -> Void, + contextAction: ((String, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil, + inlineActions: InlineActionsState? = nil + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + self.title = title + self.subtitle = subtitle + self.hashtag = hashtag + self.hasNext = hasNext + self.action = action + self.contextAction = contextAction + self.inlineActions = inlineActions + } + + public static func ==(lhs: HashtagListItemComponent, rhs: HashtagListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.hashtag != rhs.hashtag { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + if lhs.inlineActions != rhs.inlineActions { + return false + } + return true + } + + public final class View: ContextControllerSourceView, ListSectionComponent.ChildView { + public let extractedContainerView: ContextExtractedContentContainingView + private let containerButton: HighlightTrackingButton + + private let swipeOptionContainer: ListItemSwipeOptionContainer + + private let iconBackgroundLayer = SimpleLayer() + private let iconLayer = SimpleLayer() + + private let title = ComponentView() + private var label = ComponentView() + private let separatorLayer: SimpleLayer + private var avatarNode: AvatarNode? + + private let badgeBackgroundLayer = SimpleLayer() + + private var component: HashtagListItemComponent? + private weak var state: EmptyComponentState? + + public var avatarFrame: CGRect { + if let avatarNode = self.avatarNode { + return avatarNode.frame + } else { + return CGRect(origin: CGPoint(), size: CGSize()) + } + } + + public var titleFrame: CGRect? { + return self.title.view?.frame + } + + public var labelFrame: CGRect? { + guard let value = self.label.view?.frame else { + return nil + } + return value + } + + private var isExtractedToContextMenu: Bool = false + + public var customUpdateIsHighlighted: ((Bool) -> Void)? + public private(set) var separatorInset: CGFloat = 0.0 + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + self.iconBackgroundLayer.cornerRadius = 15.0 + self.badgeBackgroundLayer.cornerRadius = 4.0 + + self.extractedContainerView = ContextExtractedContentContainingView() + self.containerButton = HighlightTrackingButton() + self.containerButton.layer.anchorPoint = CGPoint() + self.containerButton.isExclusiveTouch = true + + self.swipeOptionContainer = ListItemSwipeOptionContainer(frame: CGRect()) + + super.init(frame: frame) + + self.addSubview(self.extractedContainerView) + self.targetViewForActivationProgress = self.extractedContainerView.contentView + + self.extractedContainerView.contentView.addSubview(self.swipeOptionContainer) + + self.swipeOptionContainer.addSubview(self.containerButton) + + self.layer.addSublayer(self.separatorLayer) + + self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in + guard let self else { + return + } + + self.containerButton.clipsToBounds = value + self.containerButton.backgroundColor = nil + self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0 + } + self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in + guard let self else { + return + } + self.isExtractedToContextMenu = value + + let mappedTransition: ComponentTransition + if value { + mappedTransition = ComponentTransition(transition) + } else { + mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } + self.state?.updated(transition: mappedTransition) + } + + self.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + gesture.cancel() + return + } + component.contextAction?(component.hashtag, self.extractedContainerView, gesture) + } + + self.containerButton.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if let customUpdateIsHighlighted = self.customUpdateIsHighlighted { + customUpdateIsHighlighted(highlighted) + } + } + + self.swipeOptionContainer.updateRevealOffset = { [weak self] offset, transition in + guard let self else { + return + } + transition.setBounds(view: self.containerButton, bounds: CGRect(origin: CGPoint(x: -offset, y: 0.0), size: self.containerButton.bounds.size)) + } + self.swipeOptionContainer.revealOptionSelected = { [weak self] option, _ in + guard let self, let component = self.component else { + return + } + guard let inlineActions = component.inlineActions else { + return + } + self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true) + if let inlineAction = inlineActions.actions.first(where: { $0.id == option.key }) { + inlineAction.action() + } + } + + self.containerButton.layer.addSublayer(self.iconBackgroundLayer) + self.iconBackgroundLayer.addSublayer(self.iconLayer) + + self.containerButton.layer.addSublayer(self.badgeBackgroundLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action("\(component.hashtag) ", self) + } + + func update(component: HashtagListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + var synchronousLoad = false + if let hint = transition.userData(TransitionHint.self) { + synchronousLoad = hint.synchronousLoad + } + + self.isGestureEnabled = false + + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + self.state = state + + let labelData: (String, UIColor) + if let subtitle = component.subtitle { + labelData = (subtitle, component.theme.list.itemSecondaryTextColor) + } else { + labelData = ("", .clear) + } + + let contextInset: CGFloat + if self.isExtractedToContextMenu { + contextInset = 12.0 + } else { + contextInset = 0.0 + } + + let height: CGFloat = 42.0 + let titleFont: UIFont = Font.semibold(14.0) + let subtitleFont: UIFont = Font.regular(14.0) + + let verticalInset: CGFloat = 1.0 + let leftInset: CGFloat = 55.0 + let rightInset: CGFloat = 16.0 + + let avatarSize: CGFloat = 30.0 + let avatarFrame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + + if let peer = component.peer { + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarFont) + avatarNode.isLayerBacked = false + avatarNode.isUserInteractionEnabled = false + self.avatarNode = avatarNode + self.containerButton.layer.insertSublayer(avatarNode.layer, at: 0) + } + + if avatarNode.bounds.isEmpty { + avatarNode.frame = avatarFrame + } else { + transition.setFrame(layer: avatarNode.layer, frame: avatarFrame) + } + + if peer.smallProfileImage != nil { + avatarNode.setPeerV2( + context: component.context, + theme: component.theme, + peer: peer, + authorOfMessage: nil, + overrideImage: nil, + emptyColor: nil, + clipStyle: .round, + synchronousLoad: synchronousLoad, + displayDimensions: CGSize(width: avatarSize, height: avatarSize) + ) + } else { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: .round, synchronousLoad: synchronousLoad, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } + self.iconBackgroundLayer.isHidden = true + } else { + self.iconBackgroundLayer.isHidden = false + } + + let previousTitleFrame = self.title.view?.frame + + let titleAvailableWidth = availableSize.width - leftInset - rightInset + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: titleFont, textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: titleAvailableWidth, height: 100.0) + ) + + let labelAvailableWidth = availableSize.width - leftInset - rightInset + let labelColor: UIColor = labelData.1 + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: labelData.0, font: subtitleFont, textColor: labelColor)) + )), + environment: {}, + containerSize: CGSize(width: labelAvailableWidth, height: 100.0) + ) + + let titleVerticalOffset: CGFloat = 0.0 + let centralContentHeight: CGFloat + if labelSize.height > 0.0 { + centralContentHeight = titleSize.height + labelSize.height + } else { + centralContentHeight = titleSize.height + } + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleVerticalOffset + floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.containerButton.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + } + + if let labelView = self.label.view { + let labelFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY), size: labelSize) + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + labelView.layer.anchorPoint = CGPoint() + self.containerButton.addSubview(labelView) + + labelView.center = labelFrame.origin + } else { + transition.setPosition(view: labelView, position: labelFrame.origin) + } + + labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size) + } + + if self.iconLayer.contents == nil { + self.iconLayer.contents = UIImage(bundleImageName: "Chat/Hashtag/SuggestHashtag")?.cgImage + } + + if themeUpdated { + let accentColor = UIColor(rgb: 0x007aff) + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + self.iconBackgroundLayer.backgroundColor = accentColor.cgColor + self.iconLayer.layerTintColor = UIColor.white.cgColor + self.badgeBackgroundLayer.backgroundColor = accentColor.cgColor + } + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + let iconSize = CGSize(width: 30.0, height: 30.0) + self.iconBackgroundLayer.frame = CGRect(origin: CGPoint(x: 12.0, y: floor((height - 30.0) / 2.0)), size: iconSize) + self.iconLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 30.0, height: 30.0)) + + let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.extractedContainerView, frame: resultBounds) + transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds) + self.extractedContainerView.contentRect = resultBounds + + let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0)) + + let swipeOptionContainerFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.swipeOptionContainer, frame: swipeOptionContainerFrame) + + transition.setPosition(view: self.containerButton, position: containerFrame.origin) + transition.setBounds(view: self.containerButton, bounds: CGRect(origin: self.containerButton.bounds.origin, size: containerFrame.size)) + + self.separatorInset = leftInset + + self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0) + + var rightOptions: [ListItemSwipeOptionContainer.Option] = [] + if let inlineActions = component.inlineActions { + rightOptions = inlineActions.actions.map { action in + let color: UIColor + let textColor: UIColor + switch action.color { + case .destructive: + color = component.theme.list.itemDisclosureActions.destructive.fillColor + textColor = component.theme.list.itemDisclosureActions.destructive.foregroundColor + } + + return ListItemSwipeOptionContainer.Option( + key: action.id, + title: action.title, + icon: .none, + color: color, + textColor: textColor + ) + } + } + self.swipeOptionContainer.setRevealOptions(([], rightOptions)) + + return CGSize(width: availableSize.width, height: height) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift index 3051274d1a..4dd642f7f4 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift @@ -130,10 +130,10 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, cha case .hashtag: break default: - signal = .single({ _ in return .hashtags([]) }) + signal = .single({ _ in return .hashtags([], query) }) } } else { - signal = .single({ _ in return .hashtags([]) }) + signal = .single({ _ in return .hashtags([], query) }) } let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.messages.recentlyUsedHashtags() @@ -145,7 +145,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, cha result.append(hashtag) } } - return { _ in return .hashtags(result) } + return { _ in return .hashtags(result, query) } } |> castError(ChatContextQueryError.self) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 3c678b4622..b481ee2ecf 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -472,6 +472,7 @@ public final class MessageInputPanelComponent: Component { private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:] private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:] + private var contextQueryPeer: EnginePeer? private var contextQueryResultPanel: ComponentView? private var stickersResultPanel: ComponentView? @@ -643,6 +644,17 @@ public final class MessageInputPanelComponent: Component { } let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, chatLocation: component.chatLocation, currentQueryStates: &self.contextQueryStates) + if self.contextQueryPeer == nil, let peerId = component.chatLocation?.peerId { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, peer?.addressName != nil else { + return + } + self.contextQueryPeer = peer + self.state?.updated(transition: .immediate) + }) + } + for (kind, update) in contextQueryUpdates { switch update { case .remove: @@ -1871,6 +1883,8 @@ public final class MessageInputPanelComponent: Component { var contextResults: ContextResultPanelComponent.Results? if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty { contextResults = .mentions(mentions) + } else if let result = self.contextQueryResults[.hashtag], case let .hashtags(hashtags, query) = result, !hashtags.isEmpty || (query.count >= 4 && self.contextQueryPeer != nil) { + contextResults = .hashtags(self.contextQueryPeer, hashtags, query) } if let result = self.contextQueryResults[.emoji], case let .stickers(stickers) = result, !stickers.isEmpty { @@ -1989,7 +2003,23 @@ public final class MessageInputPanelComponent: Component { } } case let .hashtag(hashtag): - let _ = hashtag + var hashtagQueryRange: NSRange? + inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) { + if type == [.hashtag] { + hashtagQueryRange = range + break inner + } + } + + if let range = hashtagQueryRange { + let inputText = NSMutableAttributedString(attributedString: inputState.inputText) + + let replacementText = hashtag + inputText.replaceCharacters(in: range, with: replacementText) + + let selectionPosition = range.lowerBound + (replacementText as NSString).length + textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition) + } } } } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 539f4497e5..5fa8c37f6f 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -52,6 +52,23 @@ final class PeerAllowedReactionsScreenComponent: Component { return true } + private struct EmojiSearchResult { + var groups: [EmojiPagerContentComponent.ItemGroup] + var id: AnyHashable + var version: Int + var isPreset: Bool + } + + private struct EmojiSearchState { + var result: EmojiSearchResult? + var isSearching: Bool + + init(result: EmojiSearchResult?, isSearching: Bool) { + self.result = result + self.isSearching = isSearching + } + } + final class View: UIView, UIScrollViewDelegate { private let scrollView: UIScrollView private let switchItem = ComponentView() @@ -84,6 +101,19 @@ final class PeerAllowedReactionsScreenComponent: Component { private var emojiContent: EmojiPagerContentComponent? private var emojiContentDisposable: Disposable? + + private let emojiSearchDisposable = MetaDisposable() + private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) + private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { + didSet { + self.emojiSearchState.set(.single(self.emojiSearchStateValue)) + } + } + + private var emptyResultEmojis: [TelegramMediaFile] = [] + private var stableEmptyResultEmoji: TelegramMediaFile? + private let stableEmptyResultEmojiDisposable = MetaDisposable() + private var caretPosition: Int? private var displayInput: Bool = false @@ -410,15 +440,43 @@ final class PeerAllowedReactionsScreenComponent: Component { chatPeerId: nil, selectedItems: Set(), backgroundIconColor: nil, - hasSearch: false, + hasSearch: true, forceHasPremium: true ) - self.emojiContentDisposable = (emojiContent - |> deliverOnMainQueue).start(next: { [weak self] emojiContent in + self.emojiContentDisposable = (combineLatest(queue: .mainQueue(), + emojiContent, + self.emojiSearchState.get() + ) + |> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchState in guard let self else { return } - self.emojiContent = emojiContent + guard let environment = self.environment else { + return + } + + var emojiContent = emojiContent + if let emojiSearchResult = emojiSearchState.result { + var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { + if self.stableEmptyResultEmoji == nil { + self.stableEmptyResultEmoji = self.emptyResultEmojis.randomElement() + } + emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: environment.strings.EmojiSearch_SearchReactionsEmptyResult, + iconFile: self.stableEmptyResultEmoji + ) + } else { + self.stableEmptyResultEmoji = nil + } + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active) + } else { + self.stableEmptyResultEmoji = nil + + if emojiSearchState.isSearching { + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiContent.contentItemGroups, itemContentUniqueId: emojiContent.itemContentUniqueId, emptySearchResults: emojiContent.emptySearchResults, searchState: .searching) + } + } emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] _, item, _, _, _, _ in @@ -531,7 +589,365 @@ final class PeerAllowedReactionsScreenComponent: Component { }, requestUpdate: { _ in }, - updateSearchQuery: { _ in + updateSearchQuery: { [weak self] query in + guard let self, let component = self.component else { + return + } + + switch query { + case .none: + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + case let .text(rawQuery, languageCode): + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) + + if query.isEmpty { + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + } else { + let context = component.context + let isEmojiOnly = !"".isEmpty + + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) + if !languageCode.lowercased().hasPrefix("en") { + signal = signal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) + } + } + + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + + let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError> + do { + let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) + |> then( + context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map { + ($0, true) + } + ) + let localPacksSignal: Signal = context.engine.stickers.searchEmojiSets(query: query) + + resultSignal = signal + |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + if isEmojiOnly { + var items: [EmojiPagerContentComponent.Item] = [] + for (_, list) in EmojiPagerContentComponent.staticEmojiMapping { + for emojiString in list { + if allEmoticons[emojiString] != nil { + let item = EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(emojiString), + itemFile: nil, + subgroupId: nil, + icon: .none, + tintMode: .none + ) + items.append(item) + } + } + } + var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )) + return .single(resultGroups) + } else { + let remoteSignal = context.engine.stickers.searchEmoji(emojiString: Array(allEmoticons.keys)) + + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), + context.engine.stickers.availableReactions() |> take(1), + hasPremium |> take(1), + remotePacksSignal, + remoteSignal, + localPacksSignal + ) + |> map { view, availableReactions, hasPremium, foundPacks, foundEmoji, foundLocalPacks -> [EmojiPagerContentComponent.ItemGroup] in + var result: [(String, TelegramMediaFile?, String)] = [] + + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + + for itemFile in foundEmoji.items { + for attribute in itemFile.attributes { + switch attribute { + case let .CustomEmoji(_, _, alt, _): + if !alt.isEmpty, let keyword = allEmoticons[alt] { + result.append((alt, itemFile, keyword)) + } else if alt == query { + result.append((alt, itemFile, alt)) + } + default: + break + } + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, _, alt, _): + if !item.file.isPremiumEmoji { + if !alt.isEmpty, let keyword = allEmoticons[alt] { + result.append((alt, item.file, keyword)) + } else if alt == query { + result.append((alt, item.file, alt)) + } + } + default: + break + } + } + } + + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for item in result { + if let itemFile = item.1 { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: (!hasPremium && itemFile.isPremiumEmoji) ? .locked : .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + } + + var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )) + + var combinedSets: FoundStickerSets + combinedSets = foundLocalPacks + combinedSets = combinedSets.merge(with: foundPacks.sets) + + var existingCollectionIds = Set() + for (collectionId, info, _, _) in combinedSets.infos { + if !existingCollectionIds.contains(collectionId) { + existingCollectionIds.insert(collectionId) + } else { + continue + } + + if let info = info as? StickerPackCollectionInfo { + var topItems: [StickerPackItem] = [] + for e in combinedSets.entries { + if let item = e.item as? StickerPackItem { + if e.index.collectionId == collectionId { + topItems.append(item) + } + } + } + + var groupItems: [EmojiPagerContentComponent.Item] = [] + for item in topItems { + var tintMode: EmojiPagerContentComponent.Item.TintMode = .none + if item.file.isCustomTemplateEmoji { + tintMode = .primary + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: (!hasPremium && item.file.isPremiumEmoji) ? .locked : .none, + tintMode: tintMode + ) + + groupItems.append(resultItem) + } + + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: AnyHashable(info.id), + groupId: AnyHashable(info.id), + title: info.title, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: 3, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: groupItems + )) + } + } + return resultGroups + } + } + } + } + + var version = 0 + self.emojiSearchStateValue.isSearching = true + self.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + version += 1 + })) + } + case let .category(value): + let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> + do { + resultSignal = component.context.engine.stickers.searchEmoji(category: value) + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for itemFile in files { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + + return .single(([EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )], isFinalResult)) + } + } + + var version = 0 + self.emojiSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + //self.emojiSearchStateValue.isSearching = true + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [ + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: true, + items: [] + ) + ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + version += 1 + })) + } }, updateScrollingToItemGroup: { }, @@ -548,6 +964,8 @@ final class PeerAllowedReactionsScreenComponent: Component { addImage: nil ) + self.emojiContent = emojiContent + if !self.isUpdating { self.state?.updated(transition: .immediate) } @@ -936,7 +1354,8 @@ final class PeerAllowedReactionsScreenComponent: Component { footer: AnyComponent(MultilineTextComponent( text: .plain(paidReactionsFooterText), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") @@ -1151,6 +1570,7 @@ final class PeerAllowedReactionsScreenComponent: Component { animateIn = true reactionSelectionControl = ComponentView() self.reactionSelectionControl = reactionSelectionControl + reactionSelectionControl.parentState = state } let reactionSelectionControlSize = reactionSelectionControl.update( transition: animateIn ? .immediate : transition, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 28ee1394a3..785ce6231f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -10,6 +10,7 @@ public enum PeerInfoPaneKey: Int32 { case members case stories case storyArchive + case gifts case media case savedMessagesChats case savedMessages @@ -20,7 +21,6 @@ public enum PeerInfoPaneKey: Int32 { case gifs case groupsInCommon case recommended - case gifts } public struct PeerInfoStatusData: Equatable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index af19ba5bb0..15fb7ee5c1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -149,6 +149,7 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/UrlHandling", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 692c16fd51..647c1d93a2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -383,6 +383,7 @@ final class PeerInfoScreenData { let starsRevenueStatsState: StarsRevenueStats? let starsRevenueStatsContext: StarsRevenueStatsContext? let revenueStatsState: RevenueStats? + let revenueStatsContext: RevenueStatsContext? let profileGiftsContext: ProfileGiftsContext? let premiumGiftOptions: [PremiumGiftCodeOption] @@ -431,6 +432,7 @@ final class PeerInfoScreenData { starsRevenueStatsState: StarsRevenueStats?, starsRevenueStatsContext: StarsRevenueStatsContext?, revenueStatsState: RevenueStats?, + revenueStatsContext: RevenueStatsContext?, profileGiftsContext: ProfileGiftsContext?, premiumGiftOptions: [PremiumGiftCodeOption] ) { @@ -467,6 +469,7 @@ final class PeerInfoScreenData { self.starsRevenueStatsState = starsRevenueStatsState self.starsRevenueStatsContext = starsRevenueStatsContext self.revenueStatsState = revenueStatsState + self.revenueStatsContext = revenueStatsContext self.profileGiftsContext = profileGiftsContext self.premiumGiftOptions = premiumGiftOptions } @@ -962,6 +965,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, starsRevenueStatsState: nil, starsRevenueStatsContext: nil, revenueStatsState: nil, + revenueStatsContext: nil, profileGiftsContext: nil, premiumGiftOptions: [] ) @@ -1009,6 +1013,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsRevenueStatsState: nil, starsRevenueStatsContext: nil, revenueStatsState: nil, + revenueStatsContext: nil, profileGiftsContext: nil, premiumGiftOptions: [] )) @@ -1025,7 +1030,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError> let profileGiftsContext: ProfileGiftsContext? if case .user = kind { - profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId) + if isMyProfile || userPeerId != context.account.peerId { + profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId) + } else { + profileGiftsContext = nil + } premiumGiftOptions = .single([]) |> then( context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) @@ -1260,7 +1269,15 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let starsRevenueContextAndState = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> mapToSignal { peer -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in - guard let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal else { + var canViewStarsRevenue = false + if let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal { + canViewStarsRevenue = true + } + #if DEBUG + canViewStarsRevenue = "".isEmpty + #endif + + guard canViewStarsRevenue else { return .single((nil, nil)) } let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId) @@ -1269,6 +1286,30 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return (starsRevenueStatsContext, state.stats) } } + + let revenueContextAndState = combineLatest( + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> distinctUntilChanged, + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId)) + |> distinctUntilChanged + ) + |> mapToSignal { peer, canViewRevenue -> Signal<(RevenueStatsContext?, RevenueStats?), NoError> in + var canViewRevenue = canViewRevenue + if let peer, case let .user(user) = peer, let _ = user.botInfo, context.sharedContext.applicationBindings.appBuildType == .internal { + canViewRevenue = true + } + #if DEBUG + canViewRevenue = "".isEmpty + #endif + guard canViewRevenue else { + return .single((nil, nil)) + } + let revenueStatsContext = RevenueStatsContext(account: context.account, peerId: peerId) + return revenueStatsContext.state + |> map { state -> (RevenueStatsContext?, RevenueStats?) in + return (revenueStatsContext, state.stats) + } + } return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), @@ -1287,9 +1328,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: false), privacySettings, starsRevenueContextAndState, + revenueContextAndState, premiumGiftOptions ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1306,7 +1348,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes?.insert(.stories, at: 0) } - if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData { + if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData, peerView.peerId != context.account.peerId { if let starGiftsCount = cachedData.starGiftsCount, starGiftsCount > 0 { availablePanes?.insert(.gifts, at: hasStories ? 1 : 0) } @@ -1405,7 +1447,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsState: nil, starsRevenueStatsState: starsRevenueContextAndState.1, starsRevenueStatsContext: starsRevenueContextAndState.0, - revenueStatsState: nil, + revenueStatsState: revenueContextAndState.1, + revenueStatsContext: revenueContextAndState.0, profileGiftsContext: profileGiftsContext, premiumGiftOptions: premiumGiftOptions ) @@ -1617,6 +1660,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsRevenueStatsState: starsRevenueContextAndState.1, starsRevenueStatsContext: starsRevenueContextAndState.0, revenueStatsState: revenueContextAndState.1, + revenueStatsContext: revenueContextAndState.0, profileGiftsContext: nil, premiumGiftOptions: [] ) @@ -1919,6 +1963,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen starsRevenueStatsState: nil, starsRevenueStatsContext: nil, revenueStatsState: nil, + revenueStatsContext: nil, profileGiftsContext: nil, premiumGiftOptions: [] )) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 15b5abe989..f6e23c1cd9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -1619,7 +1619,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { controlsClippingFrame = apparentAvatarFrame } - let avatarClipOffset: CGFloat = !self.isAvatarExpanded && deviceMetrics.hasDynamicIsland && self.avatarClippingNode.clipsToBounds && !isLandscape ? 48.0 : 0.0 + let avatarClipOffset: CGFloat = !self.isAvatarExpanded && deviceMetrics.hasDynamicIsland && self.avatarClippingNode.clipsToBounds && !isLandscape ? 47.0 : 0.0 let clippingNodeTransition = ContainedViewLayoutTransition.immediate clippingNodeTransition.updateFrame(layer: self.avatarClippingNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: avatarClipOffset), size: CGSize(width: width, height: 1000.0))) clippingNodeTransition.updateSublayerTransformOffset(layer: self.avatarClippingNode.layer, offset: CGPoint(x: 0.0, y: -avatarClipOffset)) @@ -2106,7 +2106,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { navigationTransition.updateAlpha(node: self.buttonsContainerNode, alpha: backgroundBannerAlpha) - let bannerFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + apparentBackgroundHeight), size: CGSize(width: width, height: 2000.0)) + let bannerInset: CGFloat = 3.0 + let bannerFrame = CGRect(origin: CGPoint(x: -bannerInset, y: -2000.0 + apparentBackgroundHeight), size: CGSize(width: width + bannerInset * 2.0, height: 2000.0)) if additive { transition.updateFrameAdditive(view: self.backgroundBannerView, frame: bannerFrame) @@ -2128,16 +2129,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { patternTransitionFraction: buttonsTransitionFraction * backgroundTransitionFraction )), environment: {}, - containerSize: CGSize(width: width, height: apparentBackgroundHeight) + containerSize: CGSize(width: width + bannerInset * 2.0, height: apparentBackgroundHeight + bannerInset) ) if let backgroundCoverView = self.backgroundCover.view { if backgroundCoverView.superview == nil { self.backgroundBannerView.addSubview(backgroundCoverView) } if additive { - transition.updateFrameAdditive(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - backgroundCoverSize.height), size: backgroundCoverSize)) + transition.updateFrameAdditive(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: -3.0, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize)) } else { - transition.updateFrame(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - backgroundCoverSize.height), size: backgroundCoverSize)) + transition.updateFrame(view: backgroundCoverView, frame: CGRect(origin: CGPoint(x: 0.0, y: bannerFrame.height - backgroundCoverSize.height - bannerInset), size: backgroundCoverSize)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 0ab7c72463..46f8f53d00 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -13,6 +13,8 @@ import PeerInfoVisualMediaPaneNode import PeerInfoPaneNode import PeerInfoChatListPaneNode import PeerInfoChatPaneNode +import TextFormat +import EmojiTextAttachmentView final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey @@ -41,6 +43,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { private let titleNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode + private var iconLayers: [InlineStickerItemLayer] = [] private var isSelected: Bool = false @@ -64,10 +67,46 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { self.pressed() } - func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) { + func updateText(context: AccountContext, title: String, icons: [TelegramMediaFile] = [], isSelected: Bool, presentationData: PresentationData) { self.isSelected = isSelected self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) + if !icons.isEmpty { + if self.iconLayers.isEmpty { + for icon in icons { + let iconSize = CGSize(width: 24.0, height: 24.0) + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: icon.fileId.id, + file: icon + ) + + let animationLayer = InlineStickerItemLayer( + context: .account(context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: icon, + cache: context.animationCache, + renderer: context.animationRenderer, + unique: true, + placeholderColor: presentationData.theme.list.mediaPlaceholderColor, + pointSize: iconSize, + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + self.iconLayers.append(animationLayer) + self.layer.addSublayer(animationLayer) + } + } + } else { + for layer in self.iconLayers { + layer.removeFromSuperlayer() + } + self.iconLayers.removeAll() + } + self.buttonNode.accessibilityLabel = title self.buttonNode.accessibilityTraits = [.button] if isSelected { @@ -76,9 +115,22 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { } func updateLayout(height: CGFloat) -> CGFloat { + var totalWidth: CGFloat = 0.0 let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) - return titleSize.width + totalWidth = titleSize.width + + if !self.iconLayers.isEmpty { + totalWidth += 1.0 + let iconSize = CGSize(width: 24.0, height: 24.0) + let spacing: CGFloat = 1.0 + for iconlayer in self.iconLayers { + iconlayer.frame = CGRect(origin: CGPoint(x: totalWidth, y: 12.0), size: iconSize) + totalWidth += iconSize.width + spacing + } + totalWidth -= spacing + } + return totalWidth } func updateArea(size: CGSize, sideInset: CGFloat) { @@ -89,6 +141,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { struct PeerInfoPaneSpecifier: Equatable { var key: PeerInfoPaneKey var title: String + var icons: [TelegramMediaFile] } private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { @@ -96,6 +149,7 @@ private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGF } final class PeerInfoPaneTabsContainerNode: ASDisplayNode { + private let context: AccountContext private let scrollNode: ASScrollNode private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:] private let selectedLineNode: ASImageNode @@ -104,7 +158,8 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { var requestSelectPane: ((PeerInfoPaneKey) -> Void)? - override init() { + init(context: AccountContext) { + self.context = context self.scrollNode = ASScrollNode() self.selectedLineNode = ASImageNode() @@ -153,7 +208,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode { }) self.paneNodes[specifier.key] = paneNode } - paneNode.updateText(specifier.title, isSelected: selectedPane == specifier.key, presentationData: presentationData) + paneNode.updateText(context: self.context, title: specifier.title, icons: specifier.icons, isSelected: selectedPane == specifier.key, presentationData: presentationData) } var removeKeys: [PeerInfoPaneKey] = [] for (key, _) in self.paneNodes { @@ -598,7 +653,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear) self.coveringBackgroundNode.isUserInteractionEnabled = false - self.tabsContainerNode = PeerInfoPaneTabsContainerNode() + self.tabsContainerNode = PeerInfoPaneTabsContainerNode(context: context) self.tabsSeparatorNode = ASDisplayNode() self.tabsSeparatorNode.isLayerBacked = true @@ -1122,6 +1177,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in let title: String + var icons: [TelegramMediaFile] = [] switch key { case .stories: title = presentationData.strings.PeerInfo_PaneStories @@ -1153,8 +1209,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat title = presentationData.strings.PeerInfo_SavedMessagesTabTitle case .gifts: title = presentationData.strings.PeerInfo_PaneGifts + icons = data?.profileGiftsContext?.currentState?.gifts.prefix(3).map { $0.gift.file } ?? [] } - return PeerInfoPaneSpecifier(key: key, title: title) + return PeerInfoPaneSpecifier(key: key, title: title, icons: icons) }, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition) for (_, pane) in self.pendingPanes { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index f499db410e..1c97f1bf09 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -562,6 +562,7 @@ private final class PeerInfoInteraction { let editingOpenNameColorSetup: () -> Void let editingOpenInviteLinksSetup: () -> Void let editingOpenDiscussionGroupSetup: () -> Void + let editingOpenRevenue: () -> Void let editingOpenStars: () -> Void let openParticipantsSection: (PeerInfoParticipantsSection) -> Void let openRecentActions: () -> Void @@ -629,6 +630,7 @@ private final class PeerInfoInteraction { editingOpenNameColorSetup: @escaping () -> Void, editingOpenInviteLinksSetup: @escaping () -> Void, editingOpenDiscussionGroupSetup: @escaping () -> Void, + editingOpenRevenue: @escaping () -> Void, editingOpenStars: @escaping () -> Void, openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, openRecentActions: @escaping () -> Void, @@ -695,6 +697,7 @@ private final class PeerInfoInteraction { self.editingOpenNameColorSetup = editingOpenNameColorSetup self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup + self.editingOpenRevenue = editingOpenRevenue self.editingOpenStars = editingOpenStars self.openParticipantsSection = openParticipantsSection self.openRecentActions = openRecentActions @@ -1205,6 +1208,7 @@ private enum InfoSection: Int, CaseIterable { case calls case personalChannel case peerInfo + case balances case peerInfoTrailing case peerMembers } @@ -1502,23 +1506,40 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } + let revenueBalance = data.revenueStatsState?.balances.currentBalance ?? 0 + let overallRevenueBalance = data.revenueStatsState?.balances.overallRevenue ?? 0 + let starsBalance = data.starsRevenueStatsState?.balances.currentBalance ?? 0 let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0 - if overallStarsBalance > 0 { - var string = "" + if overallRevenueBalance > 0 || overallStarsBalance > 0 { + //TODO:localize + items[.balances]!.append(PeerInfoScreenHeaderItem(id: 20, text: presentationData.strings.PeerInfo_BotBalance_Title)) + if overallRevenueBalance > 0 { + let string = "*\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))" + let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + if let range = attributedString.string.range(of: "*") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + items[.balances]!.append(PeerInfoScreenDisclosureItem(id: 21, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_BotBalance_Ton, icon: PresentationResourcesSettings.ton, action: { + interaction.editingOpenRevenue() + })) + } + if overallStarsBalance > 0 { - string.append("*\(presentationStringsFormattedNumber(Int32(starsBalance), presentationData.dateTimeFormat.groupingSeparator))") + let string = "*\(presentationStringsFormattedNumber(Int32(starsBalance), presentationData.dateTimeFormat.groupingSeparator))" + let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + if let range = attributedString.string.range(of: "*") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + items[.balances]!.append(PeerInfoScreenDisclosureItem(id: 22, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_BotBalance_Stars, icon: PresentationResourcesSettings.stars, action: { + interaction.editingOpenStars() + })) } - let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) - if let range = attributedString.string.range(of: "*") { - attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) - attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) - } - - items[currentPeerInfoSection]!.append(PeerInfoScreenDisclosureItem(id: 9, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: { - interaction.editingOpenStars() - })) + } else { + print() } if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { @@ -1715,7 +1736,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if overallRevenueBalance > 0 || overallStarsBalance > 0 { var string = "" if overallRevenueBalance > 0 { - string.append("#\(formatTonAmountText(revenueBalance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))") + string.append("#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))") } if overallStarsBalance > 0 { if !string.isEmpty { @@ -2769,6 +2790,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro editingOpenDiscussionGroupSetup: { [weak self] in self?.editingOpenDiscussionGroupSetup() }, + editingOpenRevenue: { [weak self] in + self?.editingOpenRevenue() + }, editingOpenStars: { [weak self] in self?.editingOpenStars() }, @@ -3474,6 +3498,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -5197,7 +5223,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let navigationController = self.controller?.navigationController as? NavigationController else { return } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: self.peerId, message: nil, updatedPresentationData: self.controller?.updatedPresentationData), navigationController: navigationController, forceExternal: false, openPeer: { [weak self] peer, navigation in + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: self.peerId, message: nil, updatedPresentationData: self.controller?.updatedPresentationData), navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { [weak self] peer, navigation in guard let strongSelf = self else { return } @@ -5243,7 +5269,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let result: ResolvedUrl = external ? .externalUrl(url) : tempResolved - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: forceExternal, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: forceExternal, forceUpdate: false, openPeer: { peer, navigation in self?.openPeer(peerId: peer.id, navigation: navigation) commit() }, sendFile: nil, @@ -5420,11 +5446,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let peerId = self.peerId let params = WebAppParameters(source: .settings, peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: bot.flags.contains(.hasSettings), fullSize: true) - var openUrlImpl: ((String, Bool, @escaping () -> Void) -> Void)? + var openUrlImpl: ((String, Bool, Bool, @escaping () -> Void) -> Void)? var presentImpl: ((ViewController, Any?) -> Void)? - let controller = standaloneWebAppController(context: context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { url, concealed, commit in - openUrlImpl?(url, concealed, commit) + let controller = standaloneWebAppController(context: context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { url, concealed, forceUpdate, commit in + openUrlImpl?(url, concealed, forceUpdate, commit) }, requestSwitchInline: { _, _, _ in }, getNavigationController: { [weak self] in return (self?.controller?.navigationController as? NavigationController) ?? context.sharedContext.mainWindow?.viewController as? NavigationController @@ -5432,7 +5458,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro controller.navigationPresentation = .flatModal self.controller?.push(controller) - openUrlImpl = { [weak self, weak controller] url, concealed, commit in + openUrlImpl = { [weak self, weak controller] url, concealed, forceUpdate, commit in let _ = openUserGeneratedUrl(context: context, peerId: peerId, url: url, concealed: concealed, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, openResolved: { result in @@ -5442,7 +5468,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } else if let current = controller?.navigationController as? NavigationController { navigationController = current } - context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: forceUpdate, openPeer: { peer, navigation in if let navigationController { PeerInfoScreenImpl.openPeer(context: context, peerId: peer.id, navigation: navigation, navigationController: navigationController) } @@ -6203,7 +6229,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } @@ -6412,7 +6438,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } @@ -6542,7 +6568,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } @@ -6575,6 +6601,22 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }))) } + var canReport = true + if case .creator = group.role { + canReport = false + } + if canReport { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.ReportPeer_Report, icon: { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, f in + self?.openReport(type: .default, contextController: c, backAction: { c in + if let mainItemsImpl = mainItemsImpl { + c.setItems(mainItemsImpl() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true) + } + }) + }))) + } + let clearPeerHistory = ClearPeerHistory(context: strongSelf.context, peer: group, chatPeer: group, cachedData: strongSelf.data?.cachedData) if clearPeerHistory.canClearForMyself != nil || clearPeerHistory.canClearForEveryone != nil { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_ClearMessages, icon: { theme in @@ -8392,7 +8434,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let controller = self.controller else { return } - self.context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: "", adminRights: nil), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, forceExternal: false, openPeer: { id, navigation in + self.context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: "", adminRights: nil), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { id, navigation in }, sendFile: nil, sendSticker: nil, @@ -8521,6 +8563,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(channelDiscussionGroupSetupController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) } + private func editingOpenRevenue() { + guard let revenueContext = self.data?.revenueStatsContext else { + return + } + let controller = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, section: .monetization, existingRevenueContext: revenueContext, boostStatus: nil) + + self.controller?.push(controller) + } + private func editingOpenStars() { guard let revenueContext = self.data?.starsRevenueStatsContext else { return @@ -10317,7 +10368,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if case let .instantView(webPage, _) = resolvedUrl, let customAnchor = anchor { resolvedUrl = .instantView(webPage, customAnchor) } - strongSelf.context.sharedContext.openResolvedUrl(resolvedUrl, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(resolvedUrl, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak self] controller, arguments in self?.controller?.push(controller) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) @@ -12706,7 +12757,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.controllerNode.resetHeaderExpansion() } } else { - self.controllerNode.updateNavigation(transition: .immediate, additive: false, animateHeader: false) + self.controllerNode.updateNavigation(transition: .animated(duration: 0.15, curve: .easeInOut), additive: false, animateHeader: false) } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 3cf859f663..1cdc3d2a22 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -98,8 +98,8 @@ final class StorySearchGridScreenComponent: Component { } else { let paneNodeScope: PeerInfoStoryPaneNode.Scope switch component.scope { - case let .query(query): - paneNodeScope = .search(query: query) + case let .query(peer, query): + paneNodeScope = .search(peerId: peer?.id, query: query) case let .location(coordinates, venue): paneNodeScope = .location(coordinates: coordinates, venue: venue) } @@ -273,8 +273,12 @@ public final class StorySearchGridScreen: ViewControllerComponentContainer { title = nil } switch self.scope { - case let .query(query): - self.titleView?.titleContent = .custom("\(query)", title, false) + case let .query(peer, query): + if let peer, let addressName = peer.addressName { + self.titleView?.titleContent = .custom("\(query)@\(addressName)", title, false) + } else { + self.titleView?.titleContent = .custom("\(query)", title, false) + } case .location: self.titleView?.titleContent = .custom(presentationData.strings.StoryGridScreen_TitleLocationSearch, nil, false) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index e05bcfec47..feed0fde86 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -38,7 +38,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private let backgroundNode: ASDisplayNode private let scrollNode: ASScrollNode - private var unlockBackground: ASDisplayNode? + private var unlockBackground: NavigationBackgroundNode? private var unlockSeparator: ASDisplayNode? private var unlockText: ComponentView? private var unlockButton: SolidRoundedButtonNode? @@ -263,7 +263,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.theme = presentationData.theme let unlockText: ComponentView - let unlockBackground: ASDisplayNode + let unlockBackground: NavigationBackgroundNode let unlockSeparator: ASDisplayNode let unlockButton: SolidRoundedButtonNode if let current = self.unlockText { @@ -276,7 +276,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr if let current = self.unlockBackground { unlockBackground = current } else { - unlockBackground = ASDisplayNode() + unlockBackground = NavigationBackgroundNode(color: presentationData.theme.rootController.tabBar.backgroundColor) self.addSubnode(unlockBackground) self.unlockBackground = unlockBackground } @@ -304,7 +304,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } if themeUpdated { - unlockBackground.backgroundColor = presentationData.theme.rootController.tabBar.backgroundColor + unlockBackground.updateColor(color: presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) unlockSeparator.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor unlockButton.updateTheme(SolidRoundedButtonTheme(theme: presentationData.theme)) } @@ -326,6 +326,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr let bottomPanelHeight = bottomInset + buttonSize.height + 8.0 transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight)) + unlockBackground.update(size: CGSize(width: size.width, height: bottomPanelHeight), transition: transition.containedViewLayoutTransition) transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel)) let unlockSize = unlockText.update( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 3eaa03290d..f3c14f0fba 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1492,7 +1492,7 @@ private final class StorySearchHeaderComponent: Component { public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { public enum Scope { case peer(id: EnginePeer.Id, isSaved: Bool, isArchived: Bool) - case search(query: String) + case search(peerId: EnginePeer.Id?, query: String) case location(coordinates: MediaArea.Coordinates, venue: MediaArea.Venue) case botPreview(id: EnginePeer.Id) } @@ -1749,8 +1749,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr switch self.scope { case let .peer(id, _, isArchived): self.listSource = PeerStoryListContext(account: context.account, peerId: id, isArchived: isArchived) - case let .search(query): - self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(query)) + case let .search(peerId, query): + self.listSource = SearchStoryListContext(account: context.account, source: .hashtag(peerId, query)) case let .location(coordinates, venue): self.listSource = SearchStoryListContext(account: context.account, source: .mediaArea(.venue(coordinates: coordinates, venue: venue))) case let .botPreview(id): @@ -1770,7 +1770,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if case .peer = self.scope { let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager) - |> deliverOnMainQueue).start(next: { [weak self] count in + |> deliverOnMainQueue).start(next: { [weak self] count in guard let strongSelf = self else { return } @@ -2420,7 +2420,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.updateDisposable.dispose() self.mapDisposable?.dispose() } - + public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal { let listSource = self.listSource return Signal { subscriber in @@ -2862,6 +2862,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr continue } + var authorPeer = item.peer var isReorderable = false switch self.scope { case .botPreview: @@ -2870,16 +2871,20 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if id == self.context.account.peerId { isReorderable = state.pinnedIds.contains(item.storyItem.id) } + case let .search(peerId, _): + if peerId != nil { + authorPeer = nil + } default: break } - + mappedItems.append(VisualMediaItem( index: mappedItems.count, peer: peerReference, storyId: item.id, story: item.storyItem, - authorPeer: item.peer, + authorPeer: authorPeer, isPinned: state.pinnedIds.contains(item.storyItem.id), localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue, isReorderable: isReorderable @@ -4104,7 +4109,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if self.isProfileEmbedded, case .botPreview = self.scope { self.view.backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { - self.view.backgroundColor = .clear + if case let .search(peerId, _) = self.scope, peerId != nil { + + } else { + self.view.backgroundColor = .clear + } } } } diff --git a/submodules/TelegramUI/Components/RasterizedCompositionComponent/BUILD b/submodules/TelegramUI/Components/RasterizedCompositionComponent/BUILD new file mode 100644 index 0000000000..a18681c9a5 --- /dev/null +++ b/submodules/TelegramUI/Components/RasterizedCompositionComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "RasterizedCompositionComponent", + module_name = "RasterizedCompositionComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/UIKitRuntimeUtils", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/RasterizedCompositionComponent/Sources/RasterizedCompositionComponent.swift b/submodules/TelegramUI/Components/RasterizedCompositionComponent/Sources/RasterizedCompositionComponent.swift new file mode 100644 index 0000000000..cc45809ad1 --- /dev/null +++ b/submodules/TelegramUI/Components/RasterizedCompositionComponent/Sources/RasterizedCompositionComponent.swift @@ -0,0 +1,388 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import UIKitRuntimeUtils +import ComponentFlow + +open class RasterizedCompositionLayer: CALayer { + private final class SublayerReference { + weak var layer: CALayer? + + init(layer: CALayer) { + self.layer = layer + } + } + + private var sublayerReferences: [SublayerReference] = [] + + public var onUpdatedIsAnimating: (() -> Void)? + public var onContentsUpdated: (() -> Void)? + + override public var position: CGPoint { + didSet { + if self.position != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var bounds: CGRect { + didSet { + if self.bounds != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var transform: CATransform3D { + didSet { + if !CATransform3DEqualToTransform(self.transform, oldValue) { + self.onContentsUpdated?() + } + } + } + + override public var opacity: Float { + didSet { + if self.opacity != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var isHidden: Bool { + didSet { + if self.isHidden != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var backgroundColor: CGColor? { + didSet { + if let lhs = self.backgroundColor, let rhs = oldValue { + if lhs != rhs { + self.onContentsUpdated?() + } + } else if (self.backgroundColor == nil) != (oldValue == nil) { + self.onContentsUpdated?() + } + } + } + + override public var cornerRadius: CGFloat { + didSet { + if self.cornerRadius != oldValue { + self.onContentsUpdated?() + } + } + } + + override public var masksToBounds: Bool { + didSet { + if self.masksToBounds != oldValue { + self.onContentsUpdated?() + } + } + } + + public var hasAnimationsInTree: Bool { + if let animationKeys = self.animationKeys(), !animationKeys.isEmpty { + return true + } + if let sublayers = self.sublayers { + for sublayer in sublayers { + if let sublayer = sublayer as? RasterizedCompositionLayer { + if sublayer.hasAnimationsInTree { + return true + } + } + } + } + return false + } + + override public init() { + super.init() + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func onLayerAdded(layer: CALayer) { + if !self.sublayerReferences.contains(where: { $0.layer === layer }) { + self.sublayerReferences.append(SublayerReference(layer: layer)) + } + if let layer = layer as? RasterizedCompositionLayer { + layer.onUpdatedIsAnimating = { [weak self] in + self?.onUpdatedIsAnimating?() + } + layer.onContentsUpdated = { [weak self] in + self?.onContentsUpdated?() + } + } else { + assertionFailure() + } + + self.onUpdatedIsAnimating?() + self.onContentsUpdated?() + } + + private func cleanupSublayerReferences() { + for i in (0 ..< self.sublayerReferences.count).reversed() { + if let layer = sublayerReferences[i].layer { + if layer.superlayer !== self { + sublayerReferences.remove(at: i) + } + } else { + sublayerReferences.remove(at: i) + } + } + } + + override public func addSublayer(_ layer: CALayer) { + super.addSublayer(layer) + + self.onLayerAdded(layer: layer) + } + + override public func insertSublayer(_ layer: CALayer, at idx: UInt32) { + super.insertSublayer(layer, at: idx) + + self.onLayerAdded(layer: layer) + } + + override public func insertSublayer(_ layer: CALayer, below sibling: CALayer?) { + super.insertSublayer(layer, below: sibling) + + self.onLayerAdded(layer: layer) + } + + override public func insertSublayer(_ layer: CALayer, above sibling: CALayer?) { + super.insertSublayer(layer, above: sibling) + + self.onLayerAdded(layer: layer) + } + + override public func replaceSublayer(_ oldLayer: CALayer, with newLayer: CALayer) { + super.replaceSublayer(oldLayer, with: newLayer) + + self.onLayerAdded(layer: newLayer) + } + + override public func add(_ anim: CAAnimation, forKey key: String?) { + let anim = anim.copy() as! CAAnimation + let completion = anim.completion + anim.completion = { [weak self] flag in + completion?(flag) + + guard let self else { + return + } + self.onUpdatedIsAnimating?() + } + + super.add(anim, forKey: key) + } + + override public func removeAllAnimations() { + super.removeAllAnimations() + + self.onUpdatedIsAnimating?() + } + + override public func removeAnimation(forKey key: String) { + super.removeAnimation(forKey: key) + + if let animationKeys = self.animationKeys(), !animationKeys.isEmpty { + } else { + self.onUpdatedIsAnimating?() + } + } +} + +public final class RasterizedCompositionImageLayer: RasterizedCompositionLayer { + public var image: UIImage? { + didSet { + if self.image !== oldValue { + if let image = self.image { + let capInsets = image.capInsets + if capInsets.left.isZero && capInsets.top.isZero && capInsets.right.isZero && capInsets.bottom.isZero { + self.contentsScale = image.scale + self.contents = image.cgImage + } else { + ASDisplayNodeSetResizableContents(self, image) + } + } else { + self.contents = nil + } + self.onContentsUpdated?() + } + } + } +} + +private func calculateSublayerBounds(layer: CALayer) -> CGRect { + var result: CGRect + if layer.contents != nil { + result = layer.bounds + } else { + result = CGRect() + } + + if let sublayers = layer.sublayers { + for sublayer in sublayers { + let sublayerBounds = sublayer.convert(sublayer.bounds, to: layer) + if result.isEmpty { + result = sublayerBounds + } else { + result = result.union(sublayerBounds) + } + } + } + + return result +} + +public final class RasterizedCompositionMonochromeLayer: SimpleLayer { + public let contentsLayer = RasterizedCompositionLayer() + public let maskedLayer = SimpleLayer() + public let rasterizedLayer = SimpleLayer() + + private var isContentsUpdateScheduled: Bool = false + private var isRasterizationModeUpdateScheduled: Bool = false + + override public init() { + super.init() + + self.maskedLayer.opacity = 0.0 + self.addSublayer(self.maskedLayer) + + self.maskedLayer.mask = self.contentsLayer + self.maskedLayer.rasterizationScale = UIScreenScale + + self.contentsLayer.backgroundColor = UIColor.black.cgColor + if let filter = makeLuminanceToAlphaFilter() { + self.contentsLayer.filters = [filter] + } + self.contentsLayer.rasterizationScale = UIScreenScale + + self.addSublayer(self.rasterizedLayer) + + self.contentsLayer.onContentsUpdated = { [weak self] in + guard let self else { + return + } + if !self.contentsLayer.hasAnimationsInTree { + self.scheduleContentsUpdate() + } + } + + self.contentsLayer.onUpdatedIsAnimating = { [weak self] in + guard let self else { + return + } + self.scheduleUpdateRasterizationMode() + } + + self.isContentsUpdateScheduled = true + self.isRasterizationModeUpdateScheduled = true + self.setNeedsLayout() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public init(layer: Any) { + super.init(layer: layer) + } + + private func scheduleContentsUpdate() { + self.isContentsUpdateScheduled = true + self.setNeedsLayout() + } + + private func scheduleUpdateRasterizationMode() { + self.isRasterizationModeUpdateScheduled = true + self.setNeedsLayout() + } + + override public func layoutSublayers() { + super.layoutSublayers() + + if self.isRasterizationModeUpdateScheduled { + self.isRasterizationModeUpdateScheduled = false + self.updateRasterizationMode() + } + if self.isContentsUpdateScheduled { + self.isContentsUpdateScheduled = false + if !self.contentsLayer.hasAnimationsInTree { + self.updateContents() + } + } + } + + private func updateContents() { + var contentBounds = calculateSublayerBounds(layer: self.contentsLayer) + contentBounds.size.width = ceil(contentBounds.width) + contentBounds.size.height = ceil(contentBounds.height) + self.rasterizedLayer.frame = contentBounds + let contentsImage = generateImage(contentBounds.size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.translateBy(x: -contentBounds.minX, y: -contentBounds.minY) + + self.contentsLayer.render(in: context) + }) + + if let contentsImage { + if let context = DrawingContext(size: contentsImage.size, scale: 0.0, opaque: false, clear: true), let alphaContext = DrawingContext(size: contentsImage.size, scale: 0.0, opaque: false, clear: true) { + context.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + + c.clear(CGRect(origin: CGPoint(), size: context.size)) + contentsImage.draw(in: CGRect(origin: CGPoint(), size: context.size), blendMode: .normal, alpha: 1.0) + } + alphaContext.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + + c.clear(CGRect(origin: CGPoint(), size: context.size)) + contentsImage.draw(in: CGRect(origin: CGPoint(), size: context.size), blendMode: .normal, alpha: 1.0) + } + context.blt(alphaContext, at: CGPoint(), mode: .AlphaFromColor) + + self.rasterizedLayer.contents = context.generateImage()?.cgImage + } + } else { + self.rasterizedLayer.contents = nil + } + } + + private func updateRasterizationMode() { + self.maskedLayer.opacity = self.contentsLayer.hasAnimationsInTree ? 1.0 : 0.0 + if self.rasterizedLayer.isHidden != (self.maskedLayer.opacity != 0.0) { + self.rasterizedLayer.isHidden = self.maskedLayer.opacity != 0.0 + if !self.rasterizedLayer.isHidden { + self.updateContents() + } + } + } +} diff --git a/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD b/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD new file mode 100644 index 0000000000..aa4cbd29ef --- /dev/null +++ b/submodules/TelegramUI/Components/SectionTitleContextItem/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SectionTitleContextItem", + module_name = "SectionTitleContextItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ContextUI", + "//submodules/TelegramPresentationData", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift b/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift new file mode 100644 index 0000000000..03cb0e9a23 --- /dev/null +++ b/submodules/TelegramUI/Components/SectionTitleContextItem/Sources/SectionTitleContextItem.swift @@ -0,0 +1,89 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ContextUI +import TelegramPresentationData + +public final class SectionTitleContextItem: ContextMenuCustomItem { + let text: String + + public init(text: String) { + self.text = text + } + + public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return SectionTitleContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) + } +} + +private final class SectionTitleContextItemNode: ASDisplayNode, ContextMenuCustomNode { + private let item: SectionTitleContextItem + private let presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let backgroundNode: ASDisplayNode + private let textNode: ImmediateTextNode + + var needsSeparator: Bool { + return false + } + + init(presentationData: PresentationData, item: SectionTitleContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.item = item + self.presentationData = presentationData + self.getController = getController + self.actionSelected = actionSelected + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isAccessibilityElement = false + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + self.textNode.attributedText = NSAttributedString(string: item.text, font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) + self.textNode.maximumNumberOfLines = 1 + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + } + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 16.0 + + let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - sideInset, height: .greatestFiniteMagnitude)) + let height: CGFloat = 28.0 + + return (CGSize(width: textSize.width + sideInset + sideInset, height: height), { size, transition in + let verticalOrigin = floor((size.height - textSize.height) / 2.0) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize) + transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } + + func updateTheme(presentationData: PresentationData) { + self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.sectionSeparatorColor + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 12.0 / 17.0) + self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.secondaryColor) + } + + func canBeHighlighted() -> Bool { + return false + } + + func updateIsHighlighted(isHighlighted: Bool) { + } + + func performAction() { + } +} diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift index bcc480988f..fb4c0c5aa3 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoContentComponent.swift @@ -192,6 +192,7 @@ public final class ArchiveInfoContentComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift index 16ed898dd4..b1fdea8c21 100644 --- a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerContentComponent.swift @@ -189,6 +189,7 @@ public final class BirthdayPickerContentComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: mainText.string.contains(">") ? UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0) : .zero, highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 20e342ee4d..203a6b76b1 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -616,6 +616,7 @@ final class ChatbotSetupScreenComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.25, highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift index de5d8b52b9..1d385f0986 100644 --- a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift @@ -108,7 +108,7 @@ private final class PeerBadgeComponent: Component { let _ = self.background.update( transition: transition, - component: AnyComponent(RoundedRectangle(color: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: nil)), + component: AnyComponent(RoundedRectangle(color: component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), cornerRadius: nil)), environment: {}, containerSize: size ) @@ -246,25 +246,10 @@ private final class CollectibleItemInfoScreenContentComponent: Component { let dateText = stringForDate(timestamp: username.info.purchaseDate, strings: environment.strings) - let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(username.info.cryptoCurrencyAmount, currency: username.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( - symbol: "~", - thousandsSeparator: ",", - decimalSeparator: ".", - symbolOnLeft: true, - spaceBetweenAmountAndSymbol: false, - decimalDigits: 9 - )) - var cryptoCurrencyText = rawCryptoCurrencyText - while cryptoCurrencyText.hasSuffix("0") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } - if cryptoCurrencyText.hasSuffix(".") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } + let cryptoCurrencyText = formatTonAmountText(username.info.cryptoCurrencyAmount, dateTimeFormat: environment.dateTimeFormat) + let currencyText = formatTonUsdValue(username.info.currencyAmount, divide: false, rate: 0.01, dateTimeFormat: environment.dateTimeFormat) - let (currencyText, currencySign, _) = formatCurrencyAmountCustom(username.info.currencyAmount, currency: username.info.currency) - - let rawTextString = environment.strings.CollectibleItemInfo_UsernameText("@\(username.username)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + let rawTextString = environment.strings.CollectibleItemInfo_UsernameText("@\(username.username)", environment.strings.CollectibleItemInfo_StoreName, dateText, "~\(cryptoCurrencyText)", currencyText) textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) for range in rawTextString.ranges { switch range.index { @@ -292,25 +277,10 @@ private final class CollectibleItemInfoScreenContentComponent: Component { let dateText = stringForDate(timestamp: phoneNumber.info.purchaseDate, strings: environment.strings) - let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.cryptoCurrencyAmount, currency: phoneNumber.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( - symbol: "~", - thousandsSeparator: ",", - decimalSeparator: ".", - symbolOnLeft: true, - spaceBetweenAmountAndSymbol: false, - decimalDigits: 9 - )) - var cryptoCurrencyText = rawCryptoCurrencyText - while cryptoCurrencyText.hasSuffix("0") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } - if cryptoCurrencyText.hasSuffix(".") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } + let cryptoCurrencyText = formatTonAmountText(phoneNumber.info.cryptoCurrencyAmount, dateTimeFormat: environment.dateTimeFormat) + let currencyText = formatTonUsdValue(phoneNumber.info.currencyAmount, divide: false, rate: 0.01, dateTimeFormat: environment.dateTimeFormat) - let (currencyText, currencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.currencyAmount, currency: phoneNumber.info.currency) - - let rawTextString = environment.strings.CollectibleItemInfo_PhoneText("\(formattedPhoneNumber)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + let rawTextString = environment.strings.CollectibleItemInfo_PhoneText("\(formattedPhoneNumber)", environment.strings.CollectibleItemInfo_StoreName, dateText, "~\(cryptoCurrencyText)", currencyText) textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) for range in rawTextString.ranges { switch range.index { diff --git a/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift b/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift index a96132f715..bd7a4fef68 100644 --- a/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift +++ b/submodules/TelegramUI/Components/SliderContextItem/Sources/SliderContextItem.swift @@ -8,12 +8,14 @@ import TelegramPresentationData import AnimatedCountLabelNode public final class SliderContextItem: ContextMenuCustomItem { + private let title: String? private let minValue: CGFloat private let maxValue: CGFloat private let value: CGFloat private let valueChanged: (CGFloat, Bool) -> Void - public init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + public init(title: String? = nil, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + self.title = title self.minValue = minValue self.maxValue = maxValue self.value = value @@ -21,7 +23,7 @@ public final class SliderContextItem: ContextMenuCustomItem { } public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { - return SliderContextItemNode(presentationData: presentationData, getController: getController, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged) + return SliderContextItemNode(presentationData: presentationData, getController: getController, title: self.title, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged) } } @@ -31,12 +33,18 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, private var presentationData: PresentationData private(set) var vibrancyEffectView: UIVisualEffectView? + + private let backgroundTitleNode: ImmediateTextNode + private let dimBackgroundTitleNode: ImmediateTextNode + private let foregroundTitleNode: ImmediateTextNode + private let backgroundTextNode: ImmediateAnimatedCountLabelNode private let dimBackgroundTextNode: ImmediateAnimatedCountLabelNode private let foregroundNode: ASDisplayNode private let foregroundTextNode: ImmediateAnimatedCountLabelNode + let title: String? let minValue: CGFloat let maxValue: CGFloat var value: CGFloat = 1.0 { @@ -49,13 +57,18 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, private let hapticFeedback = HapticFeedback() - init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { + init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, title: String?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { self.presentationData = presentationData + self.title = title self.minValue = minValue self.maxValue = maxValue self.value = value self.valueChanged = valueChanged + self.backgroundTitleNode = ImmediateTextNode() + self.dimBackgroundTitleNode = ImmediateTextNode() + self.foregroundTitleNode = ImmediateTextNode() + self.backgroundTextNode = ImmediateAnimatedCountLabelNode() self.backgroundTextNode.alwaysOneDirection = true @@ -76,7 +89,6 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.isUserInteractionEnabled = true if presentationData.theme.overallDarkAppearance { - } else { let style: UIBlurEffect.Style style = .extraLight @@ -87,9 +99,12 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.vibrancyEffectView = vibrancyEffectView } + self.addSubnode(self.backgroundTitleNode) + self.addSubnode(self.dimBackgroundTitleNode) self.addSubnode(self.backgroundTextNode) self.addSubnode(self.dimBackgroundTextNode) self.addSubnode(self.foregroundNode) + self.foregroundNode.addSubnode(self.foregroundTitleNode) self.foregroundNode.addSubnode(self.foregroundTextNode) let stringValue = "1.0x" @@ -114,6 +129,11 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, textCount += 1 } } + + self.backgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: backgroundTextColor) + self.dimBackgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: dimBackgroundTextColor) + self.foregroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: foregroundTextColor) + self.dimBackgroundTextNode.segments = dimBackgroundSegments self.backgroundTextNode.segments = backgroundSegments self.foregroundTextNode.segments = foregroundSegments @@ -179,6 +199,10 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, self.backgroundTextNode.segments = backgroundSegments self.foregroundTextNode.segments = foregroundSegments + self.backgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: backgroundTextColor) + self.dimBackgroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: dimBackgroundTextColor) + self.foregroundTitleNode.attributedText = NSAttributedString(string: self.title ?? "", font: textFont, textColor: foregroundTextColor) + let _ = self.dimBackgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) let _ = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) let _ = self.foregroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: transition.isAnimated) @@ -188,20 +212,41 @@ private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode, let valueWidth: CGFloat = 70.0 let height: CGFloat = 45.0 - var backgroundTextSize = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true) + let originalBackgroundTextSize = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true) + var backgroundTextSize = originalBackgroundTextSize backgroundTextSize.width = valueWidth + let backgroundTitleSize = self.backgroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + let _ = self.dimBackgroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + let _ = self.foregroundTitleNode.updateLayout(CGSize(width: 120.0, height: 100.0)) + return (CGSize(width: height * 3.0, height: height), { size, transition in let leftInset: CGFloat = 17.0 self.vibrancyEffectView?.frame = CGRect(origin: .zero, size: size) - let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + let backgroundTextWidth = self.backgroundTextNode.updateLayout(size: CGSize(width: 70.0, height: .greatestFiniteMagnitude), animated: true).width + + self.updateValue(transition: transition) + + let titleFrame: CGRect + let textFrame: CGRect + + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTitleSize.height) / 2.0)), size: backgroundTitleSize) + + if self.title != nil { + textFrame = CGRect(origin: CGPoint(x: size.width - leftInset - backgroundTextWidth, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + } else { + textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - backgroundTextSize.height) / 2.0)), size: backgroundTextSize) + } + + transition.updateFrameAdditive(node: self.dimBackgroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.backgroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.foregroundTitleNode, frame: titleFrame) + transition.updateFrameAdditive(node: self.dimBackgroundTextNode, frame: textFrame) transition.updateFrameAdditive(node: self.backgroundTextNode, frame: textFrame) transition.updateFrameAdditive(node: self.foregroundTextNode, frame: textFrame) - - self.updateValue(transition: transition) }) } diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index e05bba66bd..5a01234637 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -274,6 +274,19 @@ public final class StarsAvatarComponent: Component { self.iconView.isHidden = false self.avatarNode.isHidden = true self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) + case .apiLimitExtension: + self.backgroundView.image = generateGradientFilledCircleImage( + diameter: size.width, + colors: [ + UIColor(rgb: 0x32b83b).cgColor, + UIColor(rgb: 0x87d93b).cgColor + ], + direction: .vertical + ) + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + self.iconView.image = UIImage(bundleImageName: "Premium/Stars/PaidBroadcast") case .unsupported: iconInset = 7.0 self.backgroundView.image = generateGradientFilledCircleImage( diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 09afb0a4e3..fc2a18c3aa 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -788,6 +788,16 @@ public final class StarsImageComponent: Component { direction: .mirroredDiagonal ) iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) + case .apiLimitExtension: + iconBackgroundView.image = generateGradientFilledCircleImage( + diameter: imageSize.width, + colors: [ + UIColor(rgb: 0x32b83b).cgColor, + UIColor(rgb: 0x87d93b).cgColor + ], + direction: .vertical + ) + iconView.image = UIImage(bundleImageName: "Premium/Stars/PaidBroadcast") case .peer, .unsupported: iconInset = 15.0 iconBackgroundView.image = generateGradientFilledCircleImage( diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift index fb4e0e9d03..3bcf7b4b6b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift @@ -490,6 +490,8 @@ private final class ParagraphComponent: CombinedComponent { horizontalAlignment: .natural, maximumNumberOfLines: 0, lineSpacing: 0.2, + highlightColor: accentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index c49448d6af..b341040fc9 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -262,7 +262,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index af129188fe..ba8cdc8397 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -326,7 +326,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } case let .transaction(transaction, parentPeer): if let starGift = transaction.starGift { - titleText = "Gift" + titleText = strings.Stars_Transaction_Gift_Title descriptionText = "" count = transaction.count transactionId = transaction.id @@ -410,10 +410,15 @@ private final class StarsTransactionSheetContent: CombinedComponent { case .ads: titleText = strings.Stars_Transaction_TelegramAds_Title via = strings.Stars_Transaction_TelegramAds_Subtitle + case .apiLimitExtension: + titleText = strings.Stars_Transaction_TelegramBotApi_Title case .unsupported: titleText = strings.Stars_Transaction_Unsupported_Title } - if !transaction.media.isEmpty { + + if let floodskipNumber = transaction.floodskipNumber { + descriptionText = strings.Stars_Transaction_TelegramBotApi_Messages(floodskipNumber) + } else if !transaction.media.isEmpty { var description: String = "" var photoCount: Int32 = 0 var videoCount: Int32 = 0 @@ -933,7 +938,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { horizontalAlignment: .center, maximumNumberOfLines: 5, lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.2), + highlightColor: linkColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD index 2647ba93b5..e1d1eb0f04 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD @@ -47,6 +47,8 @@ swift_library( "//submodules/StatisticsUI", "//submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen", "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index ee6ff8b47f..6a883c8df1 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -28,7 +28,6 @@ final class StarsStatisticsScreenComponent: Component { let context: AccountContext let peerId: EnginePeer.Id let revenueContext: StarsRevenueStatsContext - let transactionsContext: StarsTransactionsContext let openTransaction: (StarsContext.State.Transaction) -> Void let withdraw: () -> Void let showTimeoutTooltip: (Int32) -> Void @@ -38,7 +37,6 @@ final class StarsStatisticsScreenComponent: Component { context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext, - transactionsContext: StarsTransactionsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, withdraw: @escaping () -> Void, showTimeoutTooltip: @escaping (Int32) -> Void, @@ -47,7 +45,6 @@ final class StarsStatisticsScreenComponent: Component { self.context = context self.peerId = peerId self.revenueContext = revenueContext - self.transactionsContext = transactionsContext self.openTransaction = openTransaction self.withdraw = withdraw self.showTimeoutTooltip = showTimeoutTooltip @@ -71,6 +68,21 @@ final class StarsStatisticsScreenComponent: Component { override func touchesShouldCancel(in view: UIView) -> Bool { return true } + + override var contentOffset: CGPoint { + set(value) { + var value = value + if value.y > self.contentSize.height - self.bounds.height { + value.y = max(0.0, self.contentSize.height - self.bounds.height) + self.bounces = false + } else { + self.bounces = true + } + super.contentOffset = value + } get { + return super.contentOffset + } + } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { @@ -92,7 +104,11 @@ final class StarsStatisticsScreenComponent: Component { } if let view = gestureRecognizer.view?.hitTest(gestureRecognizer.location(in: gestureRecognizer.view), with: nil) as? UIControl { - return !view.isTracking + if view is UIButton { + return true + } else { + return !view.isTracking + } } return true @@ -124,7 +140,12 @@ final class StarsStatisticsScreenComponent: Component { private let transactionsHeader = ComponentView() private let transactionsBackground = UIView() - private let transactionsView = ComponentView() + + private let panelContainer = ComponentView() + + private var allTransactionsContext: StarsTransactionsContext? + private var incomingTransactionsContext: StarsTransactionsContext? + private var outgoingTransactionsContext: StarsTransactionsContext? private var component: StarsStatisticsScreenComponent? private weak var state: EmptyComponentState? @@ -132,6 +153,12 @@ final class StarsStatisticsScreenComponent: Component { private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? private var controller: (() -> ViewController?)? + private var enableVelocityTracking: Bool = false + private var previousVelocityM1: CGFloat = 0.0 + private var previousVelocity: CGFloat = 0.0 + + private var listIsExpanded = false + private var ignoreScrolling: Bool = false private var stateDisposable: Disposable? @@ -194,10 +221,36 @@ final class StarsStatisticsScreenComponent: Component { self.stateDisposable?.dispose() } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + while true { + if currentParent == nil || currentParent === self { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollView { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollView + } + } + currentParent = currentParent?.superview + } + return result + } + func scrollToTop() { self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true) } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.enableVelocityTracking = true + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) @@ -205,57 +258,128 @@ final class StarsStatisticsScreenComponent: Component { if let view = self.chartView.view as? ListItemComponentAdaptor.View, let node = view.itemNode as? StatsGraphItemNode { node.resetInteraction() } + + if self.enableVelocityTracking { + self.previousVelocityM1 = self.previousVelocity + if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { + self.previousVelocity = CGFloat(value) + } + } + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + guard let navigationMetrics = self.navigationMetrics else { + return + } + + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + let paneAreaExpansionFinalPoint: CGFloat = panelContainerView.frame.minY - navigationMetrics.navigationHeight + if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne { + panelContainerView.transferVelocity(self.previousVelocityM1) + } + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let _ = self.navigationMetrics else { + return + } + + let paneAreaExpansionDistance: CGFloat = 32.0 + let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height + if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint { + targetContentOffset.pointee.y = paneAreaExpansionFinalPoint + self.enableVelocityTracking = false + self.previousVelocity = 0.0 + self.previousVelocityM1 = 0.0 } } private var lastScrollBounds: CGRect? private var lastBottomOffset: CGFloat? private func updateScrolling(transition: ComponentTransition) { - guard let environment = self.environment?[ViewControllerComponentContainer.Environment.self].value else { - return - } - let scrollBounds = self.scrollView.bounds - - let topContentOffset = self.scrollView.contentOffset.y - let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 - - let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) - animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) - animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) - let expansionDistance: CGFloat = 32.0 - var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance - expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) + let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height - transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) - - let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) - self.lastBottomOffset = bottomOffset - - let transactionsScrollBounds: CGRect - if let transactionsView = self.transactionsView.view { - transactionsScrollBounds = CGRect(origin: CGPoint(x: 0.0, y: scrollBounds.origin.y - transactionsView.frame.minY), size: scrollBounds.size) - } else { - transactionsScrollBounds = .zero + if let _ = self.navigationMetrics { + let topContentOffset = self.scrollView.contentOffset.y + let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 + + let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) + animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) + animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) + + let expansionDistance: CGFloat = 32.0 + var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance + expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) + + transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) + } + + let listIsExpanded = expansionDistanceFactor == 0.0 + if listIsExpanded != self.listIsExpanded { + self.listIsExpanded = listIsExpanded + if !self.isUpdating { + self.state?.updated(transition: .init(animation: .curve(duration: 0.25, curve: .slide))) + } + } } - self.lastScrollBounds = transactionsScrollBounds - let _ = self.transactionsView.updateEnvironment( + let _ = self.panelContainer.updateEnvironment( transition: transition, environment: { - StarsTransactionsPanelEnvironment( - theme: environment.theme, - strings: environment.strings, - dateTimeFormat: environment.dateTimeFormat, - containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), - isScrollable: false, - isCurrent: true, - externalScrollBounds: transactionsScrollBounds, - externalBottomOffset: bottomOffset - ) + StarsTransactionsPanelContainerEnvironment(isScrollable: isLockedAtPanels) } ) +// guard let environment = self.environment?[ViewControllerComponentContainer.Environment.self].value else { +// return +// } +// +// let scrollBounds = self.scrollView.bounds +// +// let topContentOffset = self.scrollView.contentOffset.y +// let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset)) / 20.0 +// +// let animatedTransition = ComponentTransition(animation: .curve(duration: 0.18, curve: .easeInOut)) +// animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) +// animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) +// +// let expansionDistance: CGFloat = 32.0 +// var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance +// expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) +// +// transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) +// +// let bottomOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) +// self.lastBottomOffset = bottomOffset +// +// let transactionsScrollBounds: CGRect +// if let transactionsView = self.transactionsView.view { +// transactionsScrollBounds = CGRect(origin: CGPoint(x: 0.0, y: scrollBounds.origin.y - transactionsView.frame.minY), size: scrollBounds.size) +// } else { +// transactionsScrollBounds = .zero +// } +// self.lastScrollBounds = transactionsScrollBounds +// +// let _ = self.transactionsView.updateEnvironment( +// transition: transition, +// environment: { +// StarsTransactionsPanelEnvironment( +// theme: environment.theme, +// strings: environment.strings, +// dateTimeFormat: environment.dateTimeFormat, +// containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), +// isScrollable: false, +// isCurrent: true, +// externalScrollBounds: transactionsScrollBounds, +// externalBottomOffset: bottomOffset +// ) +// } +// ) } private var isUpdating = false @@ -458,7 +582,8 @@ final class StarsStatisticsScreenComponent: Component { footer: AnyComponent(MultilineTextComponent( text: .plain(balanceInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -522,73 +647,118 @@ final class StarsStatisticsScreenComponent: Component { } contentHeight += balanceSize.height - contentHeight += 27.0 - - let transactionsHeaderSize = self.transactionsHeader.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.Stars_BotRevenue_Transactions_Title.uppercased(), - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: availableSize - ) - let transactionsHeaderFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 32.0, y: contentHeight), size: transactionsHeaderSize) - if let transactionsHeaderView = self.transactionsHeader.view { - if transactionsHeaderView.superview == nil { - self.scrollView.addSubview(transactionsHeaderView) + contentHeight += 44.0 + + var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] + if "".isEmpty { + let allTransactionsContext: StarsTransactionsContext + if let current = self.allTransactionsContext { + allTransactionsContext = current + } else { + allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .peer(component.peerId), mode: .all) + self.allTransactionsContext = allTransactionsContext } - transition.setFrame(view: transactionsHeaderView, frame: transactionsHeaderFrame) - } - contentHeight += transactionsHeaderSize.height - contentHeight += 6.0 - - self.transactionsBackground.backgroundColor = environment.theme.list.itemBlocksBackgroundColor - self.transactionsBackground.layer.cornerRadius = 11.0 - if #available(iOS 13.0, *) { - self.transactionsBackground.layer.cornerCurve = .continuous + + let incomingTransactionsContext: StarsTransactionsContext + if let current = self.incomingTransactionsContext { + incomingTransactionsContext = current + } else { + incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsTransactionsContext(allTransactionsContext), mode: .incoming) + self.incomingTransactionsContext = incomingTransactionsContext + } + + let outgoingTransactionsContext: StarsTransactionsContext + if let current = self.outgoingTransactionsContext { + outgoingTransactionsContext = current + } else { + outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsTransactionsContext(allTransactionsContext), mode: .outgoing) + self.outgoingTransactionsContext = outgoingTransactionsContext + } + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "all", + title: environment.strings.Stars_Intro_AllTransactions, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: allTransactionsContext, + isAccount: false, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "incoming", + title: environment.strings.Stars_Intro_Incoming, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: incomingTransactionsContext, + isAccount: false, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "outgoing", + title: environment.strings.Stars_Intro_Outgoing, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: outgoingTransactionsContext, + isAccount: false, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) } - let transactionsSize = self.transactionsView.update( - transition: .immediate, - component: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - transactionsContext: component.transactionsContext, - isAccount: false, - action: { transaction in - component.openTransaction(transaction) - } - )), - environment: { - StarsTransactionsPanelEnvironment( + var wasLockedAtPanels = false + if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { + if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { + wasLockedAtPanels = true + } + } + + let panelTransition = transition + if !panelItems.isEmpty { + let panelContainerInset: CGFloat = self.listIsExpanded ? 0.0 : 16.0 + let panelContainerCornerRadius: CGFloat = self.listIsExpanded ? 0.0 : 11.0 + + let panelContainerSize = self.panelContainer.update( + transition: panelTransition, + component: AnyComponent(StarsTransactionsPanelContainerComponent( theme: environment.theme, - strings: strings, + strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, - containerInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), - isScrollable: false, - isCurrent: true, - externalScrollBounds: self.lastScrollBounds ?? .zero, - externalBottomOffset: self.lastBottomOffset ?? 1000 - ) - }, - containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) - ) - self.transactionsView.parentState = state - let transactionsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - transactionsSize.width) / 2.0), y: contentHeight), size: transactionsSize) - if let panelContainerView = self.transactionsView.view { - if panelContainerView.superview == nil { - self.scrollContainerView.addSubview(panelContainerView) + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left + panelContainerInset, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right + panelContainerInset), + items: panelItems, + currentPanelUpdated: { [weak self] id, transition in + guard let self else { + return + } + self.currentSelectedPanelId = id + self.state?.updated(transition: transition) + } + )), + environment: { + StarsTransactionsPanelContainerEnvironment(isScrollable: wasLockedAtPanels) + }, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + ) + if let panelContainerView = self.panelContainer.view { + if panelContainerView.superview == nil { + self.scrollContainerView.addSubview(panelContainerView) + } + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - panelContainerSize.width) / 2.0), y: contentHeight), size: panelContainerSize)) + transition.setCornerRadius(layer: panelContainerView.layer, cornerRadius: panelContainerCornerRadius) } - transition.setFrame(view: panelContainerView, frame: transactionsFrame) + contentHeight += panelContainerSize.height + } else { + self.panelContainer.view?.removeFromSuperview() } - transition.setFrame(view: self.transactionsBackground, frame: transactionsFrame) - - contentHeight += transactionsSize.height - contentHeight += 31.0 self.ignoreScrolling = true @@ -624,7 +794,6 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { private let context: AccountContext private let peerId: EnginePeer.Id private let revenueContext: StarsRevenueStatsContext - private let transactionsContext: StarsTransactionsContext private weak var tooltipScreen: UndoOverlayController? private var timer: Foundation.Timer? @@ -633,7 +802,6 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { self.context = context self.peerId = peerId self.revenueContext = revenueContext - self.transactionsContext = context.engine.payments.peerStarsTransactionsContext(subject: .peer(peerId), mode: .all) var withdrawImpl: (() -> Void)? var buyAdsImpl: (() -> Void)? @@ -643,7 +811,6 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { context: context, peerId: peerId, revenueContext: revenueContext, - transactionsContext: self.transactionsContext, openTransaction: { transaction in openTransactionImpl?(transaction) }, @@ -700,13 +867,14 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { } let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { [weak self] c, a in self?.present(c, in: .window(.root)) - }, completion: { [weak self] url in + }, completion: { url in let presentationData = context.sharedContext.currentPresentationData.with { $0 } context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) Queue.mainQueue().after(2.0) { revenueContext.reload() - self?.transactionsContext.reload() + //TODO: + //self?.transactionsContext.reload() } }) self.present(controller, in: .window(.root)) @@ -787,9 +955,7 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) }) } - - self.transactionsContext.loadMore() - + self.scrollToTop = { [weak self] in guard let self, let componentView = self.node.hostView.componentView as? StarsStatisticsScreenComponent.View else { return diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 3200636923..88612faaa5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -16,6 +16,7 @@ import AvatarNode import BundleIconComponent import PhotoResources import StarsAvatarComponent +import LottieComponent private extension StarsContext.State.Transaction { var extendedId: String { @@ -101,6 +102,12 @@ final class StarsTransactionsListPanelComponent: Component { } private final class ScrollViewImpl: UIScrollView { + var forceDecelerating = false + + override var isDecelerating: Bool { + return self.forceDecelerating || super.isDecelerating + } + override func touchesShouldCancel(in view: UIView) -> Bool { return true } @@ -110,8 +117,9 @@ final class StarsTransactionsListPanelComponent: Component { private let scrollView: ScrollViewImpl private let measureItem = ComponentView() - private var visibleItems: [String: ComponentView] = [:] - private var separatorViews: [String: UIView] = [:] + private var visibleItems: [AnyHashable: ComponentView] = [:] + private var separatorLayers: [AnyHashable: SimpleLayer] = [:] + private var highlightLayer = SimpleLayer() private var ignoreScrolling: Bool = false @@ -144,6 +152,8 @@ final class StarsTransactionsListPanelComponent: Component { self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.highlightLayer) } required init?(coder: NSCoder) { @@ -154,6 +164,15 @@ final class StarsTransactionsListPanelComponent: Component { self.itemsDisposable?.dispose() } + func scrollToTop() -> Bool { + if self.scrollView.contentOffset.y > 0.0 { + self.scrollView.setContentOffset(CGPoint(), animated: true) + return true + } else { + return false + } + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) @@ -162,6 +181,79 @@ final class StarsTransactionsListPanelComponent: Component { func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { cancelContextGestures(view: scrollView) + if let decelerationAnimator = self.decelerationAnimator { + self.scrollView.forceDecelerating = false + self.decelerationAnimator = nil + decelerationAnimator.invalidate() + } + } + + private var decelerationAnimator: ConstantDisplayLinkAnimator? + func transferVelocity(_ velocity: CGFloat) { + if velocity <= 0.0 { + return + } + self.decelerationAnimator?.isPaused = true + let startTime = CACurrentMediaTime() + var currentOffset = self.scrollView.contentOffset + let decelerationRate: CGFloat = 0.998 + self.scrollView.forceDecelerating = true + //self.scrollViewDidEndDragging(self.scrollView, willDecelerate: true) + self.decelerationAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + guard let strongSelf = self else { + return + } + let t = CACurrentMediaTime() - startTime + var currentVelocity = velocity * 15.0 * CGFloat(pow(Double(decelerationRate), 1000.0 * t)) + currentOffset.y += currentVelocity + let maxOffset = strongSelf.scrollView.contentSize.height - strongSelf.scrollView.bounds.height + if currentOffset.y >= maxOffset { + currentOffset.y = maxOffset + currentVelocity = 0.0 + } + if currentOffset.y < 0.0 { + currentOffset.y = 0.0 + currentVelocity = 0.0 + } + + var didEnd = false + if abs(currentVelocity) < 0.1 { + strongSelf.decelerationAnimator?.isPaused = true + strongSelf.decelerationAnimator = nil + didEnd = true + } + var contentOffset = strongSelf.scrollView.contentOffset + contentOffset.y = floorToScreenPixels(currentOffset.y) + strongSelf.scrollView.setContentOffset(contentOffset, animated: false) + strongSelf.scrollViewDidScroll(strongSelf.scrollView) + if didEnd { + //strongSelf.scrollViewDidEndDecelerating(strongSelf.scrollView) + strongSelf.scrollView.forceDecelerating = false + } + }) + self.decelerationAnimator?.isPaused = false + } + + private var highlightedItemId: AnyHashable? + private func updateHighlightedItem(itemId: AnyHashable?) { + guard let environment = self.environment else { + return + } + if self.highlightedItemId == itemId { + return + } + let previousHighlightedItemId = self.highlightedItemId + self.highlightedItemId = itemId + + if let _ = previousHighlightedItemId, itemId == nil { + ComponentTransition.easeInOut(duration: 0.2).setBackgroundColor(layer: self.highlightLayer, color: .clear) + } + if let itemId, let itemView = self.visibleItems[itemId]?.view { + var highlightFrame = itemView.frame + highlightFrame.size.height += UIScreenPixel + self.highlightLayer.frame = highlightFrame + ComponentTransition.immediate.setBackgroundColor(layer: self.highlightLayer, color: environment.theme.list.itemHighlightedBackgroundColor) + } } private func updateScrolling(transition: ComponentTransition) { @@ -172,34 +264,34 @@ final class StarsTransactionsListPanelComponent: Component { var visibleBounds = environment.externalScrollBounds ?? self.scrollView.bounds visibleBounds = visibleBounds.insetBy(dx: 0.0, dy: -100.0) - var validIds = Set() + var validIds = Set() if let visibleItems = itemLayout.visibleItems(for: visibleBounds) { for index in visibleItems.lowerBound ..< visibleItems.upperBound { if index >= self.items.count { continue } let item = self.items[index] - let id = item.extendedId + let id = AnyHashable(item.extendedId) validIds.insert(id) var itemTransition = transition let itemView: ComponentView - let separatorView: UIView - if let current = self.visibleItems[id], let currentSeparator = self.separatorViews[id] { + let separatorLayer: SimpleLayer + if let current = self.visibleItems[id], let currentSeparator = self.separatorLayers[id] { itemView = current - separatorView = currentSeparator + separatorLayer = currentSeparator } else { itemTransition = .immediate itemView = ComponentView() self.visibleItems[id] = itemView - separatorView = UIView() - self.separatorViews[id] = separatorView - self.scrollView.addSubview(separatorView) + separatorLayer = SimpleLayer() + self.separatorLayers[id] = separatorLayer + self.scrollView.layer.addSublayer(separatorLayer) } - separatorView.backgroundColor = environment.theme.list.itemBlocksSeparatorColor - separatorView.isHidden = index == self.items.count - 1 + separatorLayer.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor + separatorLayer.isHidden = index == self.items.count - 1 let fontBaseDisplaySize = 17.0 @@ -207,11 +299,13 @@ final class StarsTransactionsListPanelComponent: Component { let itemSubtitle: String? var itemDate: String var itemPeer = item.peer + var itemFile: TelegramMediaFile? switch item.peer { case let .peer(peer): - if let _ = item.starGift { + if let starGift = item.starGift { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) itemSubtitle = item.count > 0 ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift + itemFile = starGift.file } else if let _ = item.giveawayMessageId { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) itemSubtitle = environment.strings.Stars_Intro_Transaction_GiveawayPrize @@ -250,8 +344,13 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle } } else { - itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title - itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle + if item.count > 0 && !item.flags.contains(.isRefund) { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentTopUp_Subtitle + } else { + itemTitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Title + itemSubtitle = environment.strings.Stars_Intro_Transaction_FragmentWithdrawal_Subtitle + } } case .premiumBot: itemTitle = environment.strings.Stars_Intro_Transaction_PremiumBotTopUp_Title @@ -259,6 +358,13 @@ final class StarsTransactionsListPanelComponent: Component { case .ads: itemTitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Title itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramAds_Subtitle + case .apiLimitExtension: + itemTitle = environment.strings.Stars_Intro_Transaction_TelegramBotApi_Title + if let floodskipNumber = item.floodskipNumber { + itemSubtitle = environment.strings.Stars_Intro_Transaction_TelegramBotApi_Messages(floodskipNumber) + } else { + itemSubtitle = nil + } case .unsupported: itemTitle = environment.strings.Stars_Intro_Transaction_Unsupported_Title itemSubtitle = nil @@ -298,15 +404,44 @@ final class StarsTransactionsListPanelComponent: Component { ))) ) if let itemSubtitle { - titleComponents.append( - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + let subtitleComponent: AnyComponent + if let itemFile { + subtitleComponent = AnyComponent( + HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(LottieComponent( + content: LottieComponent.ResourceContent( + context: component.context, + file: itemFile, + attemptSynchronously: false, + providesPlaceholder: true + ), + color: nil, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + size: CGSize(width: 20.0, height: 20.0), + loop: false + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: itemSubtitle, + font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), + textColor: environment.theme.list.itemPrimaryTextColor + )) + ))) + ], spacing: 2.0) + ) + } else { + subtitleComponent = AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: itemSubtitle, font: Font.regular(fontBaseDisplaySize * 16.0 / 17.0), textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 - ))) + )) + } + + titleComponents.append( + AnyComponentWithIdentity(id: AnyHashable(1), component: subtitleComponent) ) } titleComponents.append( @@ -335,12 +470,18 @@ final class StarsTransactionsListPanelComponent: Component { if !item.flags.contains(.isLocal) { component.action(item) } + }, + updateIsHighlighted: { [weak self] _, highlighted in + guard let self else { + return + } + self.updateHighlightedItem(itemId: highlighted ? id : nil) } )), environment: {}, - containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + containerSize: CGSize(width: itemLayout.containerWidth - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: itemLayout.itemHeight) ) - let itemFrame = itemLayout.itemFrame(for: index) + let itemFrame = itemLayout.itemFrame(for: index).offsetBy(dx: itemLayout.containerInsets.left, dy: 0.0) if let itemComponentView = itemView.view { if itemComponentView.superview == nil { if !transition.animation.isImmediate { @@ -351,11 +492,11 @@ final class StarsTransactionsListPanelComponent: Component { itemTransition.setFrame(view: itemComponentView, frame: itemFrame) } let sideInset: CGFloat = 60.0 + environment.containerInsets.left - itemTransition.setFrame(view: separatorView, frame: CGRect(x: sideInset, y: itemFrame.maxY, width: itemFrame.width - sideInset, height: UIScreenPixel)) + itemTransition.setFrame(layer: separatorLayer, frame: CGRect(x: sideInset, y: itemFrame.maxY, width: itemFrame.width - sideInset - environment.containerInsets.right, height: UIScreenPixel)) } } - var removeIds: [String] = [] + var removeIds: [AnyHashable] = [] for (id, itemView) in self.visibleItems { if !validIds.contains(id) { removeIds.append(id) @@ -366,10 +507,10 @@ final class StarsTransactionsListPanelComponent: Component { } } } - for (id, separatorView) in self.separatorViews { + for (id, separatorLayer) in self.separatorLayers { if !validIds.contains(id) { - transition.setAlpha(view: separatorView, alpha: 0.0, completion: { [weak separatorView] _ in - separatorView?.removeFromSuperview() + transition.setAlpha(layer: separatorLayer, alpha: 0.0, completion: { [weak separatorLayer] _ in + separatorLayer?.removeFromSuperlayer() }) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift index 2b1e6896cc..89f1e91ccb 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsPanelContainerComponent.swift @@ -427,6 +427,7 @@ final class StarsTransactionsPanelContainerComponent: Component { } class View: UIView, UIGestureRecognizerDelegate { + private let topPanelClippingView: UIView private let topPanelBackgroundView: UIView private let topPanelMergedBackgroundView: UIView private let topPanelSeparatorLayer: SimpleLayer @@ -436,6 +437,7 @@ final class StarsTransactionsPanelContainerComponent: Component { private weak var state: EmptyComponentState? private let panelsBackgroundLayer: SimpleLayer + private let clippingView: UIView private var visiblePanels: [AnyHashable: ComponentView] = [:] private var actualVisibleIds = Set() private var currentId: AnyHashable? @@ -443,6 +445,10 @@ final class StarsTransactionsPanelContainerComponent: Component { private var animatingTransition: Bool = false override init(frame: CGRect) { + self.topPanelClippingView = UIView() + self.topPanelClippingView.clipsToBounds = true + self.topPanelClippingView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.topPanelBackgroundView = UIView() self.topPanelMergedBackgroundView = UIView() @@ -452,11 +458,16 @@ final class StarsTransactionsPanelContainerComponent: Component { self.panelsBackgroundLayer = SimpleLayer() + self.clippingView = UIView() + self.clippingView.clipsToBounds = true + super.init(frame: frame) self.layer.addSublayer(self.panelsBackgroundLayer) - self.addSubview(self.topPanelBackgroundView) - self.addSubview(self.topPanelMergedBackgroundView) + self.addSubview(self.clippingView) + self.addSubview(self.topPanelClippingView) + self.topPanelClippingView.addSubview(self.topPanelBackgroundView) + self.topPanelClippingView.addSubview(self.topPanelMergedBackgroundView) self.layer.addSublayer(self.topPanelSeparatorLayer) let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in @@ -593,6 +604,19 @@ final class StarsTransactionsPanelContainerComponent: Component { transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value) } + func transferVelocity(_ velocity: CGFloat) { + if let currentPanelView = self.currentPanelView as? StarsTransactionsListPanelComponent.View { + currentPanelView.transferVelocity(velocity) + } + } + + func scrollToTop() -> Bool { + if let currentPanelView = self.currentPanelView as? StarsTransactionsListPanelComponent.View { + return currentPanelView.scrollToTop() + } + return false + } + func update(component: StarsTransactionsPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[StarsTransactionsPanelContainerEnvironment.self].value @@ -610,13 +634,17 @@ final class StarsTransactionsPanelContainerComponent: Component { let topPanelCoverHeight: CGFloat = 10.0 - let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0)) - transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame) - transition.setFrame(view: self.topPanelMergedBackgroundView, frame: topPanelFrame) + let containerWidth = availableSize.width - component.insets.left - component.insets.right + let topPanelFrame = CGRect(origin: CGPoint(x: component.insets.left, y: -topPanelCoverHeight), size: CGSize(width: containerWidth, height: 44.0)) + transition.setFrame(view: self.topPanelClippingView, frame: topPanelFrame) + transition.setFrame(view: self.topPanelBackgroundView, frame: CGRect(origin: .zero, size: topPanelFrame.size)) + transition.setFrame(view: self.topPanelMergedBackgroundView, frame: CGRect(origin: .zero, size: topPanelFrame.size)) + + transition.setCornerRadius(layer: self.topPanelClippingView.layer, cornerRadius: component.insets.left > 0.0 ? 11.0 : 0.0) - transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))) + transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topPanelFrame.maxY), size: CGSize(width: containerWidth, height: availableSize.height - topPanelFrame.maxY))) - transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topPanelFrame.maxY), size: CGSize(width: containerWidth, height: UIScreenPixel))) if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) { self.currentId = nil @@ -641,7 +669,7 @@ final class StarsTransactionsPanelContainerComponent: Component { } } - let sideInset: CGFloat = 16.0 + let sideInset: CGFloat = 16.0 + component.insets.left let condensedPanelWidth: CGFloat = availableSize.width - sideInset * 2.0 let headerSize = self.header.update( transition: transition, @@ -674,7 +702,7 @@ final class StarsTransactionsPanelContainerComponent: Component { if headerView.superview == nil { self.addSubview(headerView) } - transition.setFrame(view: headerView, frame: CGRect(origin: topPanelFrame.origin.offsetBy(dx: sideInset, dy: 0.0), size: headerSize)) + transition.setFrame(view: headerView, frame: CGRect(origin: topPanelFrame.origin.offsetBy(dx: 16.0, dy: 0.0), size: headerSize)) } let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)) @@ -688,7 +716,7 @@ final class StarsTransactionsPanelContainerComponent: Component { for (id, _) in self.visiblePanels { visibleIds.insert(id) } - + var validIds = Set() if let currentIndex { var anyAnchorOffset: CGFloat = 0.0 @@ -769,7 +797,7 @@ final class StarsTransactionsPanelContainerComponent: Component { ) if let panelView = panel.view { if panelView.superview == nil { - self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView) + self.clippingView.addSubview(panelView) } panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in @@ -790,6 +818,11 @@ final class StarsTransactionsPanelContainerComponent: Component { } } + let clippingFrame = CGRect(origin: CGPoint(x: component.insets.left, y: 0.0), size: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height)) + + transition.setPosition(view: self.clippingView, position: clippingFrame.center) + transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: component.insets.left, y: 0.0), size: clippingFrame.size)) + var removeIds: [AnyHashable] = [] for (id, panel) in self.visiblePanels { if !validIds.contains(id) { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 52d550b52c..22e2aa4e50 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -120,6 +120,8 @@ final class StarsTransactionsScreenComponent: Component { private var previousVelocityM1: CGFloat = 0.0 private var previousVelocity: CGFloat = 0.0 + private var listIsExpanded = false + private var ignoreScrolling: Bool = false private var stateDisposable: Disposable? @@ -183,20 +185,56 @@ final class StarsTransactionsScreenComponent: Component { self.stateDisposable?.dispose() } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + while true { + if currentParent == nil || currentParent === self { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollView { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollView + } + } + currentParent = currentParent?.superview + } + return result + } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.enableVelocityTracking = true } func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - if self.enableVelocityTracking { - self.previousVelocityM1 = self.previousVelocity - if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { - self.previousVelocity = CGFloat(value) - } + guard !self.ignoreScrolling else { + return + } + if self.enableVelocityTracking { + self.previousVelocityM1 = self.previousVelocity + if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { + self.previousVelocity = CGFloat(value) + } + } + + self.updateScrolling(transition: .immediate) + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + guard let navigationMetrics = self.navigationMetrics else { + return + } + + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + let paneAreaExpansionFinalPoint: CGFloat = panelContainerView.frame.minY - navigationMetrics.navigationHeight + if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne { + panelContainerView.transferVelocity(self.previousVelocityM1) } - - self.updateScrolling(transition: .immediate) } } @@ -214,6 +252,12 @@ final class StarsTransactionsScreenComponent: Component { self.previousVelocityM1 = 0.0 } } + + func scrollToTop() { + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View, !panelContainerView.scrollToTop() { + self.scrollView.setContentOffset(.zero, animated: true) + } + } private func updateScrolling(transition: ComponentTransition) { let scrollBounds = self.scrollView.bounds @@ -273,6 +317,14 @@ final class StarsTransactionsScreenComponent: Component { if let view = self.topBalanceIconView.view { view.alpha = topBalanceAlpha } + + let listIsExpanded = expansionDistanceFactor == 0.0 + if listIsExpanded != self.listIsExpanded { + self.listIsExpanded = listIsExpanded + if !self.isUpdating { + self.state?.updated(transition: .init(animation: .curve(duration: 0.25, curve: .slide))) + } + } } let _ = self.panelContainer.updateEnvironment( @@ -362,7 +414,7 @@ final class StarsTransactionsScreenComponent: Component { var contentHeight: CGFloat = 0.0 - let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16 * 2.0 + let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16.0 * 2.0 let bottomInset: CGFloat = environment.safeInsets.bottom contentHeight += environment.statusBarHeight @@ -834,13 +886,16 @@ final class StarsTransactionsScreenComponent: Component { } if !panelItems.isEmpty { + let panelContainerInset: CGFloat = self.listIsExpanded ? 0.0 : 16.0 + let panelContainerCornerRadius: CGFloat = self.listIsExpanded ? 0.0 : 11.0 + let panelContainerSize = self.panelContainer.update( transition: panelTransition, component: AnyComponent(StarsTransactionsPanelContainerComponent( theme: environment.theme, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, - insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left + panelContainerInset, bottom: bottomInset, right: environment.safeInsets.right + panelContainerInset), items: panelItems, currentPanelUpdated: { [weak self] id, transition in guard let self else { @@ -859,7 +914,8 @@ final class StarsTransactionsScreenComponent: Component { if panelContainerView.superview == nil { self.scrollContainerView.addSubview(panelContainerView) } - transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - panelContainerSize.width) / 2.0), y: contentHeight), size: panelContainerSize)) + transition.setCornerRadius(layer: panelContainerView.layer, cornerRadius: panelContainerCornerRadius) } contentHeight += panelContainerSize.height } else { @@ -1097,6 +1153,15 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { self.starsContext.load(force: false) self.subscriptionsContext.loadMore() + + self.scrollToTop = { [weak self] in + guard let self else { + return + } + if let componentView = self.node.hostView.componentView as? StarsTransactionsScreenComponent.View { + componentView.scrollToTop() + } + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD index 35f40fa69f..b58ec7b9b8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/TelegramUI/Components/PremiumPeerShortcutComponent", "//submodules/ConfettiEffect", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 7b9c5ce639..2db31ddab1 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -222,7 +222,8 @@ private final class SheetContent: CombinedComponent { amountFooter = AnyComponent(MultilineTextComponent( text: .plain(amountInfoString), maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) @@ -456,7 +457,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent { }) } )), - backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + backgroundColor: .color(environment.theme.list.blocksBackgroundColor), followContentSizeChanges: false, clipsContent: true, isScrollEnabled: false, diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 45352322e2..53130d45e0 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -865,6 +865,28 @@ final class StorageUsageScreenComponent: Component { self.keepScreenActiveDisposable?.dispose() } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + while true { + if currentParent == nil || currentParent === self { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollView { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollView + } + } + currentParent = currentParent?.superview + } + return result + } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.enableVelocityTracking = true } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 5c26840dfd..53974ed884 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2864,7 +2864,7 @@ public final class StoryItemSetContainerComponent: Component { style: .story, placeholder: inputPlaceholder, maxLength: 4096, - queryTypes: [.mention, .emoji], + queryTypes: [.mention, .hashtag, .emoji], alwaysDarkWhenHasText: component.metrics.widthClass == .regular, resetInputContents: resetInputContents, nextInputMode: { [weak self] hasText in @@ -4339,6 +4339,7 @@ public final class StoryItemSetContainerComponent: Component { urlContext: .generic, navigationController: nextController?.navigationController as? NavigationController, forceExternal: false, + forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 58e24479c6..c28cb72788 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1812,7 +1812,7 @@ final class StoryItemSetContainerSendMessage { let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) - controller.openUrl = { [weak self] url, _, _ in + controller.openUrl = { [weak self] url, _, _, _ in guard let self else { return } @@ -2676,6 +2676,7 @@ final class StoryItemSetContainerSendMessage { urlContext: .chat(peerId: peerId, message: nil, updatedPresentationData: updatedPresentationData), navigationController: navigationController, forceExternal: forceExternal, + forceUpdate: false, openPeer: { [weak self, weak view] peerId, navigation in guard let self, let view, let component = view.component, let controller = component.controller() as? StoryContainerScreen else { return @@ -2919,10 +2920,10 @@ final class StoryItemSetContainerSendMessage { } if !hashtag.isEmpty { if peerName == nil { - let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, scope: .query(hashtag), listContext: nil) + let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, scope: .query(nil, hashtag), listContext: nil) navigationController.pushViewController(searchController) } else { - let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) + let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, stories: true, forceDark: true) navigationController.pushViewController(searchController) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index ccc85856e5..5c2a590428 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -1114,11 +1114,11 @@ final class StoryItemSetViewListComponent: Component { let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return ("URL", "") }) let text: String - if self.configuration.listMode == .everyone && (self.query == nil || self.query == "") { + if self.configuration.listMode == .everyone && ((self.query ?? "") == "") { if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { if emptyButton == nil { if let views = component.storyItem.views, views.seenCount > 0 { - text = component.strings.Story_Views_ViewsNotRecorded + text = component.peerId.isGroupOrChannel ? component.strings.Story_Views_NoReactions : component.strings.Story_Views_ViewsNotRecorded } else { text = component.strings.Story_Views_ViewsExpired } diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index 486ab7df91..f107fd6366 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -27,14 +27,18 @@ public final class TabSelectorComponent: Component { public struct CustomLayout: Equatable { public var font: UIFont public var spacing: CGFloat + public var innerSpacing: CGFloat? public var lineSelection: Bool public var verticalInset: CGFloat + public var allowScroll: Bool - public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false, verticalInset: CGFloat = 0.0) { + public init(font: UIFont, spacing: CGFloat, innerSpacing: CGFloat? = nil, lineSelection: Bool = false, verticalInset: CGFloat = 0.0, allowScroll: Bool = true) { self.font = font self.spacing = spacing + self.innerSpacing = innerSpacing self.lineSelection = lineSelection self.verticalInset = verticalInset + self.allowScroll = allowScroll } } @@ -154,16 +158,19 @@ public final class TabSelectorComponent: Component { verticalInset = customLayout.verticalInset * 2.0 } - let innerInset: CGFloat = 12.0 - let spacing: CGFloat = component.customLayout?.spacing ?? 2.0 + var innerInset: CGFloat = component.customLayout?.innerSpacing ?? 12.0 + var spacing: CGFloat = component.customLayout?.spacing ?? 2.0 let itemFont: UIFont var isLineSelection = false + let allowScroll: Bool if let customLayout = component.customLayout { itemFont = customLayout.font isLineSelection = customLayout.lineSelection + allowScroll = customLayout.allowScroll || component.items.count > 3 } else { itemFont = Font.semibold(14.0) + allowScroll = true } if selectionColorUpdated { @@ -181,15 +188,14 @@ public final class TabSelectorComponent: Component { } } - var contentWidth: CGFloat = spacing - var previousBackgroundRect: CGRect? - var selectedBackgroundRect: CGRect? - var nextBackgroundRect: CGRect? + var innerContentWidth: CGFloat = 0.0 let selectedIndex = component.items.firstIndex(where: { $0.id == component.selectedId }) var validIds: [AnyHashable] = [] var index = 0 + var itemViews: [AnyHashable: (VisibleItem, CGSize, ComponentTransition)] = [:] + for item in component.items { var itemTransition = transition let itemView: VisibleItem @@ -243,8 +249,27 @@ public final class TabSelectorComponent: Component { environment: {}, containerSize: CGSize(width: 200.0, height: 100.0) ) - - if !contentWidth.isZero { + innerContentWidth += itemSize.width + itemViews[item.id] = (itemView, itemSize, itemTransition) + index += 1 + } + + let estimatedContentWidth = 2.0 * spacing + innerContentWidth + (CGFloat(component.items.count - 1) * (spacing + innerInset)) + if estimatedContentWidth > availableSize.width && !allowScroll { + spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1) + innerInset = 0.0 + } + + var contentWidth: CGFloat = spacing + var previousBackgroundRect: CGRect? + var selectedBackgroundRect: CGRect? + var nextBackgroundRect: CGRect? + + for item in component.items { + guard let (itemView, itemSize, itemTransition) = itemViews[item.id] else { + continue + } + if contentWidth > spacing { contentWidth += spacing } let itemTitleFrame = CGRect(origin: CGPoint(x: contentWidth + innerInset, y: verticalInset + floor((baseHeight - itemSize.height) * 0.5)), size: itemSize) @@ -269,7 +294,6 @@ public final class TabSelectorComponent: Component { itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size)) itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection || component.colors.simple ? 1.0 : 0.4) } - index += 1 } contentWidth += spacing @@ -301,7 +325,7 @@ public final class TabSelectorComponent: Component { } } - var mappedSelectionFrame = effectiveBackgroundRect.insetBy(dx: 12.0, dy: 0.0) + var mappedSelectionFrame = effectiveBackgroundRect.insetBy(dx: innerInset, dy: 0.0) mappedSelectionFrame.origin.y = mappedSelectionFrame.maxY + 6.0 mappedSelectionFrame.size.height = 3.0 transition.setFrame(view: self.selectionView, frame: mappedSelectionFrame) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/Contents.json new file mode 100644 index 0000000000..719c35fbb0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tagrecent.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/tagrecent.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/tagrecent.pdf new file mode 100644 index 0000000000..62a616bc67 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/SuggestHashtag.imageset/tagrecent.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/Contents.json new file mode 100644 index 0000000000..4999eeb431 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "edited.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/edited.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/edited.pdf new file mode 100644 index 0000000000..129aee2ff4 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/MenuEditIcon.imageset/edited.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Contents.json new file mode 100644 index 0000000000..76ccbf54aa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Improving_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Improving_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Improving_30.pdf new file mode 100644 index 0000000000..afdcc8bf28 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/ToastImprovingVideo.imageset/Improving_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json new file mode 100644 index 0000000000..b00344b3db --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "settings_24 (1).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf new file mode 100644 index 0000000000..ec496f75d6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsNoDot.imageset/settings_24 (1).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json new file mode 100644 index 0000000000..9c81bdcc4b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "play.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf new file mode 100644 index 0000000000..c3ed904f78 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/VideoRateToast.imageset/play.pdf @@ -0,0 +1,73 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 1.411133 cm +1.000000 1.000000 1.000000 scn +0.000000 8.588867 m +0.000000 0.588867 l +0.000000 -0.235178 0.940764 -0.705560 1.600000 -0.211133 c +6.933333 3.788867 l +7.466667 4.188867 7.466667 4.988867 6.933333 5.388867 c +1.600000 9.388867 l +0.940764 9.883294 0.000000 9.412912 0.000000 8.588867 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 378 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 8.000000 12.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000468 00000 n +0000000490 00000 n +0000000662 00000 n +0000000736 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +795 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/Contents.json new file mode 100644 index 0000000000..84dca5328a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "paidbroadcast.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/paidbroadcast.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/paidbroadcast.pdf new file mode 100644 index 0000000000..49f4200193 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/PaidBroadcast.imageset/paidbroadcast.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/video_toast_speedup.json b/submodules/TelegramUI/Resources/Animations/video_toast_speedup.json new file mode 100644 index 0000000000..a6a099f10f --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/video_toast_speedup.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":512,"h":512,"nm":"SpeedUp","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"L","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-149.676,-134.674,0],"ix":2,"l":2},"a":{"a":0,"k":[-149.676,-134.674,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-118.501,-90.586],[-180.85,-131.391],[-120.298,-178.762]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"R 2","parent":4,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[0]},{"t":37,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[148.867,-138.259,0],"ix":2,"l":2},"a":{"a":0,"k":[86.867,-138.259,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[55.693,-182.348],[118.041,-141.542],[57.489,-94.171]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":238,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"R","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.7,"y":0},"t":15.525,"s":[315.867,117.741,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.24,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[386.867,113.741,0],"to":[0,0,0],"ti":[0,0,0]},{"t":58,"s":[361.867,113.741,0]}],"ix":2,"l":2},"a":{"a":0,"k":[86.867,-138.259,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[55.693,-182.348],[118.041,-141.542],[57.489,-94.171]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":238,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Finger","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.8],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":15,"s":[-8]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":40.369,"s":[2]},{"t":59,"s":[0]}],"ix":10},"p":{"a":0,"k":[-18.5,36.537,0],"ix":2,"l":2},"a":{"a":0,"k":[-18.5,36.537,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0.184,-29.147],[0,0],[0,0]],"o":[[0,0],[0,0],[0.179,-28.358],[0,0],[0,0],[0,0]],"v":[[-42.122,74.94],[-42.656,-41.628],[-42.954,-106.447],[2.666,-106.159],[2.179,-28.956],[1.757,30.035]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Hand","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.8],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":15,"s":[-27]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":39,"s":[-4]},{"t":58,"s":[-8]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.788,"y":1},"o":{"x":0.675,"y":0.1},"t":15,"s":[324.228,369.807,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.2,"y":0},"t":35.711,"s":[332.157,343.048,0],"to":[0,0,0],"ti":[0,0,0]},{"t":54.341796875,"s":[331.157,340.048,0]}],"ix":2,"l":2},"a":{"a":0,"k":[28,102.537,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.8,"y":1},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[0,0],[0.191,-30.324],[3.985,-27.467],[66.688,0.421],[12.251,16.781],[45.025,26.208],[-30.441,-3.454],[-8.116,-7.778]],"o":[[0,0],[0.191,-30.324],[0,0],[-3.778,26.039],[-53.508,-0.338],[-21.42,-29.674],[-23.406,-13.363],[30.592,3.823],[0,0]],"v":[[90.706,47.786],[90.902,16.644],[132.787,16.908],[132.141,119.309],[36.922,202.939],[-54.145,150.224],[-124.225,81.972],[-99.575,44.059],[-41.999,74.829]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.2,"y":0},"t":29.5,"s":[{"i":[[0,0],[0,0],[0.191,-30.324],[3.985,-27.467],[66.688,0.421],[15.266,14.266],[43.037,9.57],[-40.297,5.526],[-13.417,-4.962]],"o":[[0,0],[0.191,-30.324],[0,0],[-3.778,26.039],[-53.508,-0.338],[-21.287,-19.893],[-27.722,-6.164],[30.686,-4.208],[0,0]],"v":[[90.706,47.786],[90.902,16.644],[132.787,16.908],[132.141,119.309],[36.922,202.939],[-54.531,154.659],[-124.202,111.503],[-111.986,60.033],[-41.999,74.829]],"c":false}]},{"t":48.130859375,"s":[{"i":[[0,0],[0,0],[0.191,-30.324],[3.985,-27.467],[66.688,0.421],[11.838,17.125],[45.297,28.484],[-29.093,-4.682],[-7.391,-8.163]],"o":[[0,0],[0.191,-30.324],[0,0],[-3.778,26.039],[-53.508,-0.338],[-21.438,-31.012],[-22.816,-14.347],[30.58,4.921],[0,0]],"v":[[90.706,47.786],[90.902,16.644],[132.787,16.908],[132.141,119.309],[36.922,202.939],[-54.092,149.617],[-124.228,77.933],[-97.877,41.873],[-41.999,74.829]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.19,-30.193],[0,0]],"o":[[0,0],[0.19,-30.193],[0,0],[0,0]],"v":[[44.474,37.139],[44.686,3.459],[90.983,3.751],[90.706,47.786]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.288,-28.763],[0,0]],"o":[[0,0],[0.181,-28.763],[0,0],[0,0]],"v":[[1.812,29.305],[2.067,-11.134],[44.776,-10.865],[44.474,37.139]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"ct":1,"bm":0}],"markers":[{"tm":0,"cm":"1","dr":0},{"tm":38,"cm":"2","dr":0}],"props":{}} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 1e853ce12b..1c9faa2e3b 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1492,6 +1492,12 @@ private func extractAccountManagerState(records: AccountRecordsView= minReindexTimestamp { + #if DEBUG && false + Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground") + let _ = self.runCacheReindexTasks(lowImpact: true, completion: { + Logger.shared.log("App \(self.episodeId)", "Executing low-impact cache reindex in foreground — done1") + }) + #endif } else { UserDefaults.standard.set(timestamp as NSNumber, forKey: "TelegramCacheIndexTimestamp") diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 8805580805..b10cae7bec 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -180,11 +180,17 @@ extension ChatControllerImpl { } + self.chatDisplayNode.historyNode.hasAtLeast3MessagesUpdated = { [weak self] hasAtLeast3Messages in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasAtLeast3Messages(hasAtLeast3Messages) }) + } + } self.chatDisplayNode.historyNode.hasPlentyOfMessagesUpdated = { [weak self] hasPlentyOfMessages in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(interactive: false, { $0.updatedHasPlentyOfMessages(hasPlentyOfMessages) }) } } + if case .peer(self.context.account.peerId) = self.chatLocation { var didDisplayTooltip = false if "".isEmpty { @@ -1120,9 +1126,16 @@ extension ChatControllerImpl { self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f, messageCorrelationId in //print("setup layoutActionOnViewTransition") - self?.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in + guard let self else { + return + } + self.layoutActionOnViewTransitionAction = f + + self.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in f() if let strongSelf = self, let validLayout = strongSelf.validLayout { + strongSelf.layoutActionOnViewTransitionAction = nil + var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? let isScheduledMessages: Bool @@ -1262,35 +1275,75 @@ extension ChatControllerImpl { } } - let signal: Signal<[MessageId?], NoError> - if forwardSourcePeerIds.count > 1 { - var signals: [Signal<[MessageId?], NoError>] = [] - for messagesGroup in forwardedMessages { - signals.append(enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messagesGroup)) - } - signal = combineLatest(signals) - |> map { results in - var ids: [MessageId?] = [] - for result in results { - ids.append(contentsOf: result) + let _ = (strongSelf.shouldDivertMessagesToScheduled(messages: transformedMessages) + |> deliverOnMainQueue).start(next: { shouldDivert in + let signal: Signal<[MessageId?], NoError> + var shouldOpenScheduledMessages = false + if forwardSourcePeerIds.count > 1 { + var forwardedMessages = forwardedMessages + if shouldDivert { + forwardedMessages = forwardedMessages.map { messageGroup -> [EnqueueMessage] in + return messageGroup.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes + } + } + } + shouldOpenScheduledMessages = true } - return ids + + var signals: [Signal<[MessageId?], NoError>] = [] + for messagesGroup in forwardedMessages { + signals.append(enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: messagesGroup)) + } + signal = combineLatest(signals) + |> map { results in + var ids: [MessageId?] = [] + for result in results { + ids.append(contentsOf: result) + } + return ids + } + } else { + var transformedMessages = transformedMessages + if shouldDivert { + transformedMessages = transformedMessages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes + } + } + shouldOpenScheduledMessages = true + } + + signal = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages) } - } else { - signal = enqueueMessages(account: strongSelf.context.account, peerId: peerId, messages: transformedMessages) - } - - let _ = (signal - |> deliverOnMainQueue).startStandalone(next: { messageIds in - if let strongSelf = self { + + let _ = (signal + |> deliverOnMainQueue).startStandalone(next: { messageIds in + guard let strongSelf = self else { + return + } if case .scheduledMessages = strongSelf.presentationInterfaceState.subject { } else { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + + if shouldOpenScheduledMessages { + if let layoutActionOnViewTransitionAction = strongSelf.layoutActionOnViewTransitionAction { + strongSelf.layoutActionOnViewTransitionAction = nil + layoutActionOnViewTransitionAction() + } + } } - } + }) + + donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) }) - - donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) } else if case let .customChatContents(customChatContents) = strongSelf.subject { switch customChatContents.kind { case .hashTagSearch: @@ -1390,7 +1443,7 @@ extension ChatControllerImpl { if let messageId = strongSelf.presentationInterfaceState.interfaceState.editMessage?.messageId { let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) |> deliverOnMainQueue).startStandalone(next: { message in - guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState, case let .media(options) = editMessageState.content else { + guard let strongSelf = self, let editMessageState = strongSelf.presentationInterfaceState.editMessageState else { return } var originalMediaReference: AnyMediaReference? @@ -1405,7 +1458,11 @@ extension ChatControllerImpl { } } } - strongSelf.oldPresentAttachmentMenu(editMediaOptions: options, editMediaReference: originalMediaReference) + var editMediaOptions: MessageMediaEditingOptions? + if case let .media(options) = editMessageState.content { + editMediaOptions = options + } + strongSelf.presentEditingAttachmentMenu(editMediaOptions: editMediaOptions, editMediaReference: originalMediaReference) }) } else { strongSelf.presentAttachmentMenu(subject: .default) @@ -4178,12 +4235,7 @@ extension ChatControllerImpl { guard let strongSelf = self, let peerId = strongSelf.chatLocation.peerId else { return } - var langCode = langCode - if langCode == "nb" { - langCode = "no" - } else if langCode == "pt-br" { - langCode = "pt" - } + let langCode = normalizeTranslationLanguage(langCode) let _ = updateChatTranslationStateInteractively(engine: strongSelf.context.engine, peerId: peerId, { current in return current?.withToLang(langCode).withIsEnabled(true) }).startStandalone() @@ -4530,22 +4582,62 @@ extension ChatControllerImpl { if let peerId = peerId { self.sentMessageEventsDisposable.set((self.context.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId) - |> deliverOnMainQueue).startStrict(next: { [weak self] namespace, silent in - if let strongSelf = self { - let inAppNotificationSettings = strongSelf.context.sharedContext.currentInAppNotificationSettings.with { $0 } - if inAppNotificationSettings.playSounds && !silent { - serviceSoundManager.playMessageDeliveredSound() + |> deliverOnMainQueue).startStrict(next: { [weak self] eventGroup in + guard let self else { + return + } + let inAppNotificationSettings = self.context.sharedContext.currentInAppNotificationSettings.with { $0 } + if inAppNotificationSettings.playSounds, let firstEvent = eventGroup.first, !firstEvent.isSilent { + serviceSoundManager.playMessageDeliveredSound() + } + if self.presentationInterfaceState.subject != .scheduledMessages, let firstEvent = eventGroup.first, firstEvent.id.namespace == Namespaces.Message.ScheduledCloud { + if eventGroup.contains(where: { $0.isPendingProcessing }) { + self.openScheduledMessages(completion: { [weak self] c in + guard let self else { + return + } + + c.dismissAllUndoControllers() + + Queue.mainQueue().after(0.5) { [weak c] in + c?.displayProcessingVideoTooltip(messageId: firstEvent.id) + } + + c.present( + UndoOverlayController( + presentationData: self.presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Chat/ToastImprovingVideo"), color: .white)!, + size: nil, + title: self.presentationData.strings.Chat_ToastImprovingVideo_Title, + text: self.presentationData.strings.Chat_ToastImprovingVideo_Text, + customUndoText: nil, + timeout: 5.0 + ), + elevatedLayout: false, + position: .top, + action: { _ in + return true + } + ), + in: .current + ) + }) } - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && namespace == Namespaces.Message.ScheduledCloud { - strongSelf.openScheduledMessages() + } + + if self.shouldDisplayChecksTooltip { + Queue.mainQueue().after(1.0) { [weak self] in + self?.displayChecksTooltip() } - - if strongSelf.shouldDisplayChecksTooltip { - Queue.mainQueue().after(1.0) { - strongSelf.displayChecksTooltip() - } - strongSelf.shouldDisplayChecksTooltip = false - strongSelf.checksTooltipDisposable.set(strongSelf.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict()) + self.shouldDisplayChecksTooltip = false + self.checksTooltipDisposable.set(self.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .newcomerTicks).startStrict()) + } + + if let shouldDisplayProcessingVideoTooltip = self.shouldDisplayProcessingVideoTooltip { + self.shouldDisplayProcessingVideoTooltip = nil + Queue.mainQueue().after(1.0) { [weak self] in + self?.displayProcessingVideoTooltip(messageId: shouldDisplayProcessingVideoTooltip) } } })) @@ -4854,6 +4946,20 @@ extension ChatControllerImpl { }), in: .current) }) + if case .scheduledMessages = self.subject { + self.postedScheduledMessagesEventsDisposable = (self.context.account.stateManager.sentScheduledMessageIds + |> deliverOnMainQueue).start(next: { [weak self] ids in + guard let self, let peerId = self.chatLocation.peerId else { + return + } + let filteredIds = Array(ids).filter({ $0.peerId == peerId }) + if filteredIds.isEmpty { + return + } + self.displayPostedScheduledMessagesToast(ids: filteredIds) + }) + } + self.displayNodeDidLoad() } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 705b34fd36..b0d5639d4b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -105,6 +105,10 @@ extension ChatControllerImpl { } } } + + if messages.contains(where: { $0.pendingProcessingAttribute != nil }) { + tip = .videoProcessing + } if actions.tip == nil { actions.tip = tip diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index e2d930eb90..1fec78f193 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -109,8 +109,8 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u var presentImpl: ((ViewController, Any?) -> Void)? let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: 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 + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in @@ -174,10 +174,11 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } let webViewSignal: Signal + let webViewSource: RequestSimpleWebViewSource = isInline ? .inline(startParam: payload) : .generic if url.isEmpty { - webViewSignal = context.engine.messages.requestMainWebView(botId: botId, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme)) + webViewSignal = context.engine.messages.requestMainWebView(botId: botId, source: webViewSource, themeParams: generateWebAppThemeParams(presentationData.theme)) } else { - webViewSignal = context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme)) + webViewSignal = context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: webViewSource, themeParams: generateWebAppThemeParams(presentationData.theme)) } messageActionCallbackDisposable.set(((webViewSignal @@ -196,8 +197,8 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u 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: 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 + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in @@ -242,8 +243,8 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } var presentImpl: ((ViewController, Any?) -> Void)? let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, 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 + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, completion: { [weak parentController] in @@ -295,7 +296,9 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() openWebView() }, showMore: nil, openTerms: { - + if let navigationController = parentController.navigationController as? NavigationController { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } }) parentController.present(controller, in: .window(.root)) } @@ -375,7 +378,7 @@ public extension ChatControllerImpl { }) } - static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) { + static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, forceUpdate: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) { if let controller { controller.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) } else { @@ -388,7 +391,7 @@ public extension ChatControllerImpl { } else if let main = context.sharedContext.mainWindow?.viewController as? NavigationController { navigationController = main } - context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: forceUpdate, openPeer: { peer, navigation in if let navigationController { ChatControllerImpl.botOpenPeer(context: context, peerId: peer.id, navigation: navigation, navigationController: navigationController) } @@ -471,8 +474,8 @@ public extension ChatControllerImpl { let context = strongSelf.context let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize)) var presentImpl: ((ViewController, Any?) -> Void)? - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in - ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) }, requestSwitchInline: { [weak self] query, chatTypes, completion in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 602ed24931..9e251e8af9 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -10,6 +10,7 @@ import MediaPickerUI import MediaPasteboardUI import LegacyMediaPickerUI import MediaEditor +import ChatEntityKeyboardInputNode extension ChatControllerImpl { func displayPasteMenu(_ subjects: [MediaPickerScreen.Subject.Media]) { @@ -32,7 +33,13 @@ extension ChatControllerImpl { }) } }, - getSourceRect: nil + getSourceRect: nil, + makeEntityInputView: { [weak self] in + guard let self else { + return nil + } + return EntityInputView(context: self.context, isDark: false, areCustomEmojiEnabled: self.presentationInterfaceState.customEmojiAvailable) + } ) controller.navigationPresentation = .flatModal strongSelf.push(controller) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift new file mode 100644 index 0000000000..f2ddc3926b --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift @@ -0,0 +1,100 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import UndoUI +import AccountContext +import ChatControllerInteraction + +extension ChatControllerImpl { + func displayPostedScheduledMessagesToast(ids: [EngineMessage.Id]) { + let timestamp = CFAbsoluteTimeGetCurrent() + if self.lastPostedScheduledMessagesToastTimestamp + 0.4 >= timestamp { + return + } + self.lastPostedScheduledMessagesToastTimestamp = timestamp + + guard case .scheduledMessages = self.presentationInterfaceState.subject else { + return + } + + let _ = (self.context.engine.data.get( + EngineDataList(ids.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] messages in + guard let self else { + return + } + let messages = messages.compactMap { $0 } + + var found: (message: EngineMessage, file: TelegramMediaFile)? + outer: for message in messages { + for media in message.media { + if let file = media as? TelegramMediaFile, file.isVideo { + found = (message, file) + break outer + } + } + } + + guard let (message, file) = found else { + return + } + + guard case let .loaded(isEmpty, _) = self.chatDisplayNode.historyNode.currentHistoryState else { + return + } + + if isEmpty { + if let navigationController = self.navigationController as? NavigationController, let topController = navigationController.viewControllers.first(where: { c in + if let c = c as? ChatController, c.chatLocation == self.chatLocation { + return true + } + return false + }) as? ChatControllerImpl { + topController.controllerInteraction?.presentControllerInCurrent(UndoOverlayController( + presentationData: self.presentationData, + content: .media( + context: self.context, + file: .message(message: MessageReference(message._asMessage()), media: file), + title: nil, + text: self.presentationData.strings.Chat_ToastVideoPublished_Title, + undoText: nil, + customAction: nil + ), + elevatedLayout: false, + position: .top, + animateInAsReplacement: false, + action: { _ in false } + ), nil) + + self.dismiss() + } + } else { + self.controllerInteraction?.presentControllerInCurrent(UndoOverlayController( + presentationData: self.presentationData, + content: .media( + context: self.context, + file: .message(message: MessageReference(message._asMessage()), media: file), + title: nil, + text: self.presentationData.strings.Chat_ToastVideoPublished_Title, + undoText: self.presentationData.strings.Chat_ToastVideoPublished_Action, + customAction: { [weak self] in + guard let self else { + return + } + self.dismiss() + } + ), + elevatedLayout: false, + position: .top, + animateInAsReplacement: false, + action: { _ in false } + ), nil) + } + }) + } +} diff --git a/submodules/TelegramUI/Sources/ChatAdPanelNode.swift b/submodules/TelegramUI/Sources/ChatAdPanelNode.swift new file mode 100644 index 0000000000..c3e42603d3 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatAdPanelNode.swift @@ -0,0 +1,520 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import StickerResources +import PhotoResources +import TelegramStringFormatting +import AnimatedCountLabelNode +import AnimatedNavigationStripeNode +import ContextUI +import RadialStatusNode +import TextFormat +import ChatPresentationInterfaceState +import TextNodeWithEntities +import AnimationCache +import MultiAnimationRenderer +import TranslateUI +import ChatControllerInteraction + +private enum PinnedMessageAnimation { + case slideToTop + case slideToBottom +} + +final class ChatAdPanelNode: ASDisplayNode { + private let context: AccountContext + private(set) var message: Message? + + var controllerInteraction: ChatControllerInteraction? + + private let tapButton: HighlightTrackingButtonNode + + private let contextContainer: ContextControllerSourceNode + private let clippingContainer: ASDisplayNode + private let contentContainer: ASDisplayNode + private let contentTextContainer: ASDisplayNode + private let adNode: TextNode + private let titleNode: TextNode + private let textNode: TextNodeWithEntities + + private let removeButtonNode: HighlightTrackingButtonNode + private let removeBackgroundNode: ASImageNode + private let removeTextNode: ImmediateTextNode + + private let closeButton: HighlightableButtonNode + + private let imageNode: TransformImageNode + private let imageNodeContainer: ASDisplayNode + + private let separatorNode: ASDisplayNode + + private var currentLayout: (CGFloat, CGFloat, CGFloat)? + private var currentMessage: Message? + private var previousMediaReference: AnyMediaReference? + + private let fetchDisposable = MetaDisposable() + + private let animationCache: AnimationCache? + private let animationRenderer: MultiAnimationRenderer? + + init(context: AccountContext, animationCache: AnimationCache?, animationRenderer: MultiAnimationRenderer?) { + self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer + + self.tapButton = HighlightTrackingButtonNode() + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.contextContainer = ContextControllerSourceNode() + + self.clippingContainer = ASDisplayNode() + self.clippingContainer.clipsToBounds = true + + self.contentContainer = ASDisplayNode() + self.contentTextContainer = ASDisplayNode() + + self.adNode = TextNode() + self.adNode.displaysAsynchronously = false + self.adNode.isUserInteractionEnabled = false + + self.removeButtonNode = HighlightTrackingButtonNode() + self.removeBackgroundNode = ASImageNode() + + self.removeTextNode = ImmediateTextNode() + self.removeTextNode.displaysAsynchronously = false + self.removeTextNode.isUserInteractionEnabled = false + + self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = false + self.titleNode.isUserInteractionEnabled = false + + self.textNode = TextNodeWithEntities() + self.textNode.textNode.displaysAsynchronously = false + self.textNode.textNode.isUserInteractionEnabled = false + + self.imageNode = TransformImageNode() + self.imageNode.contentAnimations = [.subsequentUpdates] + + self.imageNodeContainer = ASDisplayNode() + + self.closeButton = HighlightableButtonNode() + self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + self.closeButton.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.contextContainer) + + self.contextContainer.addSubnode(self.clippingContainer) + self.clippingContainer.addSubnode(self.contentContainer) + self.contentTextContainer.addSubnode(self.titleNode) + + self.contentTextContainer.addSubnode(self.adNode) + + self.contentTextContainer.addSubnode(self.textNode.textNode) + self.contentContainer.addSubnode(self.contentTextContainer) + + self.imageNodeContainer.addSubnode(self.imageNode) + self.contentContainer.addSubnode(self.imageNodeContainer) + + self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) + self.tapButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.adNode.layer.removeAnimation(forKey: "opacity") + strongSelf.adNode.alpha = 0.4 + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.4 + strongSelf.textNode.textNode.layer.removeAnimation(forKey: "opacity") + strongSelf.textNode.textNode.alpha = 0.4 + strongSelf.imageNode.layer.removeAnimation(forKey: "opacity") + strongSelf.imageNode.alpha = 0.4 + strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.removeTextNode.alpha = 0.4 + strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.removeBackgroundNode.alpha = 0.4 + } else { + strongSelf.adNode.alpha = 1.0 + strongSelf.adNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.titleNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.textNode.textNode.alpha = 1.0 + strongSelf.textNode.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.imageNode.alpha = 1.0 + strongSelf.imageNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.removeTextNode.alpha = 1.0 + strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.removeBackgroundNode.alpha = 1.0 + strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.contextContainer.addSubnode(self.tapButton) + + self.contextContainer.addSubnode(self.removeBackgroundNode) + self.contextContainer.addSubnode(self.removeTextNode) + self.contextContainer.addSubnode(self.removeButtonNode) + + self.addSubnode(self.separatorNode) + + self.removeButtonNode.addTarget(self, action: #selector(self.removePressed), forControlEvents: [.touchUpInside]) + self.removeButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.removeTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.removeTextNode.alpha = 0.4 + strongSelf.removeBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.removeBackgroundNode.alpha = 0.4 + } else { + strongSelf.removeTextNode.alpha = 1.0 + strongSelf.removeTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.removeBackgroundNode.alpha = 1.0 + strongSelf.removeBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.contextContainer.activated = { [weak self] gesture, _ in + guard let self, let message = self.message else { + return + } + self.controllerInteraction?.adContextAction(message, self.contextContainer, gesture) + } + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + } + + deinit { + self.fetchDisposable.dispose() + } + + private var theme: PresentationTheme? + + @objc private func closePressed() { + if self.context.isPremium, let adAttribute = self.message?.adAttribute { + self.controllerInteraction?.removeAd(adAttribute.opaqueId) + } else { + self.controllerInteraction?.openNoAdsDemo() + } + } + + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + self.message = interfaceState.adMessage + + if self.theme !== interfaceState.theme { + self.theme = interfaceState.theme + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + self.removeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 15.0, color: interfaceState.theme.chat.inputPanel.panelControlAccentColor.withMultipliedAlpha(0.1)) + self.removeTextNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_BotAd_WhatIsThis, font: Font.regular(11.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor) + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(interfaceState.theme), for: []) + } + + self.contextContainer.isGestureEnabled = false + + let panelHeight: CGFloat + var hasCloseButton = true + if let message = interfaceState.adMessage { + panelHeight = self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: nil, message: message, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, dateTimeFormat: interfaceState.dateTimeFormat, accountPeerId: self.context.account.peerId, firstTime: false, isReplyThread: false, translateToLanguage: nil) + hasCloseButton = message.media.isEmpty + } else { + panelHeight = 50.0 + } + + self.contextContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) + + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) + self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) + + self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) + self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight)) + + let contentRightInset: CGFloat = 14.0 + rightInset + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + self.closeButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize) + + self.closeButton.isHidden = !hasCloseButton + + self.currentLayout = (width, leftInset, rightInset) + + return panelHeight + } + + private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, message: Message, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool, translateToLanguage: String?) -> CGFloat { + var animationTransition: ContainedViewLayoutTransition = .immediate + + if let animation = animation { + animationTransition = .animated(duration: 0.2, curve: .easeInOut) + + if let copyView = self.textNode.textNode.view.snapshotView(afterScreenUpdates: false) { + let offset: CGFloat + switch animation { + case .slideToTop: + offset = -10.0 + case .slideToBottom: + offset = 10.0 + } + + copyView.frame = self.textNode.textNode.frame + self.textNode.textNode.view.superview?.addSubview(copyView) + copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true) + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in + copyView?.removeFromSuperview() + }) + self.textNode.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true) + self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + let makeAdLayout = TextNode.asyncLayout(self.adNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode) + let imageNodeLayout = self.imageNode.asyncLayout() + + let previousMediaReference = self.previousMediaReference + let context = self.context + + let contentLeftInset: CGFloat = leftInset + 18.0 + let contentRightInset: CGFloat = rightInset + 9.0 + + var textRightInset: CGFloat = 0.0 + + var updatedMediaReference: AnyMediaReference? + var imageDimensions: CGSize? + + if !message.containsSecretMedia { + for media in message.media { + if let image = media as? TelegramMediaImage { + updatedMediaReference = .message(message: MessageReference(message), media: image) + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + break + } else if let file = media as? TelegramMediaFile { + updatedMediaReference = .message(message: MessageReference(message), media: file) + if !file.isInstantVideo && !file.isSticker, let representation = largestImageRepresentation(file.previewRepresentations) { + imageDimensions = representation.dimensions.cgSize + } else if file.isAnimated, let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } + break + } else if let paidContent = media as? TelegramMediaPaidContent, let firstMedia = paidContent.extendedMedia.first { + switch firstMedia { + case let .preview(dimensions, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + if let dimensions { + imageDimensions = dimensions.cgSize + } + updatedMediaReference = .standalone(media: thumbnailMedia) + case let .full(fullMedia): + updatedMediaReference = .message(message: MessageReference(message), media: fullMedia) + if let image = fullMedia as? TelegramMediaImage { + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.cgSize + } + break + } else if let file = fullMedia as? TelegramMediaFile { + if let dimensions = file.dimensions { + imageDimensions = dimensions.cgSize + } + break + } + } + } + } + } + + let imageBoundingSize = CGSize(width: 48.0, height: 48.0) + var applyImage: (() -> Void)? + if let imageDimensions { + applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 3.0), imageSize: imageDimensions.aspectFilled(imageBoundingSize), boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets())) + textRightInset += imageBoundingSize.width + 18.0 + } + + var mediaUpdated = false + if let updatedMediaReference = updatedMediaReference, let previousMediaReference = previousMediaReference { + mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media) + } else if (updatedMediaReference != nil) != (previousMediaReference != nil) { + mediaUpdated = true + } + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchMediaSignal: Signal? + if mediaUpdated { + if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + if imageReference.media.representations.isEmpty { + updateImageSignal = chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, ignoreFullSize: true, synchronousLoad: true) + } else { + updateImageSignal = chatMessagePhotoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), photoReference: imageReference, blurred: false) + } + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isAnimatedSticker { + let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) + updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) + updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) + } else if fileReference.media.isVideo || fileReference.media.isAnimated { + updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: false) + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + updateImageSignal = chatWebpageSnippetFile(account: context.account, userLocation: .peer(message.id.peerId), mediaReference: fileReference.abstract, representation: iconImageRepresentation) + } + } + } else { + updateImageSignal = .single({ _ in return nil }) + } + } + + let textConstrainedSize = CGSize(width: width - contentLeftInset - contentRightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) + + let (adLayout, adApply) = makeAdLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Chat_BotAd_Title, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.panelControlAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero)) + + var titleText: String = "" + if let author = message.author { + titleText = EnginePeer(author).compactDisplayTitle + } + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleText, font: Font.semibold(14.0), textColor: theme.chat.inputPanel.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero)) + + let (textString, _, isText) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(message), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, accountPeerId: accountPeerId) + + let messageText: NSAttributedString + let textFont = Font.regular(14.0) + if isText { + var text = message.text + var messageEntities = message.textEntitiesAttribute?.entities ?? [] + + if let translateToLanguage = translateToLanguage, !text.isEmpty { + for attribute in message.attributes { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { + text = attribute.text + messageEntities = attribute.entities + break + } + } + } + + let entities = messageEntities.filter { entity in + switch entity.type { + case .CustomEmoji: + return true + default: + return false + } + } + let textColor = theme.chat.inputPanel.primaryTextColor + if entities.count > 0 { + messageText = stringWithAppliedEntities(trimToLineCount(text, lineCount: 1), entities: entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message) + } else { + messageText = NSAttributedString(string: foldLineBreaks(text), font: textFont, textColor: textColor) + } + } else { + messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) + } + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: .zero)) + + var panelHeight: CGFloat = 0.0 + if let _ = imageDimensions { + panelHeight = 9.0 + imageBoundingSize.height + 9.0 + } + + var textHeight: CGFloat + var titleOnSeparateLine = false + if textLayout.numberOfLines == 1 || contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width > width - contentRightInset - textRightInset { + textHeight = adLayout.size.height + titleLayout.size.height + textLayout.size.height + 15.0 + titleOnSeparateLine = true + } else { + textHeight = titleLayout.size.height + textLayout.size.height + 15.0 + } + + panelHeight = max(panelHeight, textHeight) + + Queue.mainQueue().async { + let _ = adApply() + let _ = titleApply() + + var textArguments: TextNodeWithEntities.Arguments? + if let cache = self.animationCache, let renderer = self.animationRenderer { + textArguments = TextNodeWithEntities.Arguments( + context: self.context, + cache: cache, + renderer: renderer, + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: false + ) + } + let _ = textApply(textArguments) + + self.previousMediaReference = updatedMediaReference + + let textContainerFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: 0.0), size: CGSize(width: width, height: panelHeight)) + animationTransition.updateFrameAdditive(node: self.contentTextContainer, frame: textContainerFrame) + + let removeTextSize = self.removeTextNode.updateLayout(CGSize(width: width, height: .greatestFiniteMagnitude)) + + if titleOnSeparateLine { + self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size) + self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: titleLayout.size) + self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 43.0), size: textLayout.size) + + self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize) + self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0) + self.removeButtonNode.frame = self.removeBackgroundNode.frame + } else { + self.adNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 9.0), size: adLayout.size) + self.titleNode.frame = CGRect(origin: CGPoint(x: adLayout.size.width + 2.0, y: 9.0), size: titleLayout.size) + self.textNode.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 26.0), size: textLayout.size) + + self.removeTextNode.frame = CGRect(origin: CGPoint(x: contentLeftInset + adLayout.size.width + 2.0 + titleLayout.size.width + 8.0, y: 11.0 - UIScreenPixel), size: removeTextSize) + self.removeBackgroundNode.frame = self.removeTextNode.frame.insetBy(dx: -5.0, dy: -1.0) + self.removeButtonNode.frame = self.removeBackgroundNode.frame + } + + self.textNode.visibilityRect = CGRect.infinite + + self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: width - contentRightInset - imageBoundingSize.width, y: 9.0), size: imageBoundingSize) + self.imageNode.frame = CGRect(origin: CGPoint(), size: imageBoundingSize) + + if let applyImage = applyImage { + applyImage() + + animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 1.0) + animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 1.0, beginWithCurrentState: true) + } else { + animationTransition.updateSublayerTransformScale(node: self.imageNodeContainer, scale: 0.1) + animationTransition.updateAlpha(node: self.imageNodeContainer, alpha: 0.0, beginWithCurrentState: true) + } + + if let updateImageSignal = updateImageSignal { + self.imageNode.setSignal(updateImageSignal) + } + if let updatedFetchMediaSignal = updatedFetchMediaSignal { + self.fetchDisposable.set(updatedFetchMediaSignal.startStrict()) + } + } + + return panelHeight + } + + @objc func tapped() { + guard let message = self.message else { + return + } + self.controllerInteraction?.activateAdAction(message.id, nil, false, false) + } + + @objc func removePressed() { + guard let message = self.message else { + return + } + self.controllerInteraction?.adContextAction(message, self.contextContainer, nil) + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a4a0dff681..9c4a1ce845 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -130,6 +130,8 @@ import OwnershipTransferController import OldChannelsController import BrowserUI import NotificationPeerExceptionController +import AdsReportScreen +import AdUI public enum ChatControllerPeekActions { case standard @@ -330,6 +332,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let startingBot = ValuePromise(false, ignoreRepeated: true) let unblockingPeer = ValuePromise(false, ignoreRepeated: true) public let searching = ValuePromise(false, ignoreRepeated: true) + public let searchResultsCount = ValuePromise(0, ignoreRepeated: true) let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() let loadingMessage = Promise(nil) let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) @@ -447,6 +450,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let checksTooltipDisposable = MetaDisposable() var shouldDisplayChecksTooltip = false + var shouldDisplayProcessingVideoTooltip: EngineMessage.Id? let peerSuggestionsDisposable = MetaDisposable() let peerSuggestionsDismissDisposable = MetaDisposable() @@ -479,6 +483,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G weak var copyProtectionTooltipController: TooltipController? weak var emojiPackTooltipController: TooltipScreen? weak var birthdayTooltipController: TooltipScreen? + weak var scheduledVideoProcessingTooltipController: TooltipScreen? weak var slowmodeTooltipController: ChatSlowmodeHintController? @@ -629,6 +634,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + public var externalSearchResultsCount: Int32? { + didSet { + if let panelNode = self.chatDisplayNode.inputPanelNode as? ChatTagSearchInputPanelNode { + panelNode.externalSearchResultsCount = self.externalSearchResultsCount + } + } + } + public var includeSavedPeersInSearchResults: Bool = false { didSet { self.chatDisplayNode.includeSavedPeersInSearchResults = self.includeSavedPeersInSearchResults @@ -641,6 +654,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + var layoutActionOnViewTransitionAction: (() -> Void)? + + var lastPostedScheduledMessagesToastTimestamp: Double = 0.0 + var postedScheduledMessagesEventsDisposable: Disposable? + public init( context: AccountContext, chatLocation: ChatLocation, @@ -2360,7 +2378,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let botPeer = botPeer { let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id) - |> deliverOnMainQueue).startStandalone(next: { value in + |> deliverOnMainQueue).startStandalone(next: { [weak self] value in guard let strongSelf = self else { return } @@ -2368,12 +2386,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if value { openBot() } else { - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Conversation_BotInteractiveUrlAlert(EnginePeer(botPeer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(botPeer), completion: { [weak self] _ in if let strongSelf = self { let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openBot() } - })]), in: .window(.root), with: nil) + openBot() + }, showMore: nil, openTerms: { [weak self] in + if let self, let navigationController = self.effectiveNavigationController { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + }) + strongSelf.present(controller, in: .window(.root)) } }) } @@ -3916,7 +3939,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.openWebApp(buttonText: buttonText, url: url, simple: simple, source: source) }, activateAdAction: { [weak self] messageId, progress, media, fullscreen in - guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else { + guard let self else { + return + } + + var message: Message? + if let historyMessage = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + message = historyMessage + } else if let panelMessage = self.chatDisplayNode.adPanelNode?.message, panelMessage.id == messageId { + message = panelMessage + } + + guard let message, let adAttribute = message.adAttribute else { return } @@ -3931,6 +3965,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: media, fullscreen: fullscreen) self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: adAttribute.url, concealed: false, external: true, progress: progress)) + }, adContextAction: { [weak self] message, sourceNode, gesture in + guard let self else { + return + } + var isBot = false + if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil { + isBot = true + } + let controller = AdsInfoScreen( + context: context, + mode: isBot ? .bot : .channel, + message: message + ) + controller.removeAd = { [weak self] opaqueId in + self?.removeAd(opaqueId: opaqueId) + } + self.effectiveNavigationController?.pushViewController(controller) + }, removeAd: { [weak self] opaqueId in + guard let self else { + return + } + self.removeAd(opaqueId: opaqueId) }, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in guard let self else { return @@ -4251,6 +4307,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G adOpaqueId = adAttribute.opaqueId } } + if adOpaqueId == nil, let panelMessage = self.chatDisplayNode.adPanelNode?.message, let adAttribute = panelMessage.adAttribute { + adOpaqueId = adAttribute.opaqueId + } let _ = self.context.engine.accountData.updateAdMessagesEnabled(enabled: false).start() if let adOpaqueId { self.removeAd(opaqueId: adOpaqueId) @@ -4271,7 +4330,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - self.push(AdsInfoScreen(context: self.context)) + self.push(AdsInfoScreen(context: self.context, mode: .channel)) }, displayGiveawayParticipationStatus: { [weak self] messageId in guard let self else { return @@ -5491,6 +5550,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isPremiumRequiredForMessaging = .single(false) } + let adMessage: Signal + if let adMessagesContext = self.chatDisplayNode.historyNode.adMessagesContext { + adMessage = adMessagesContext.state |> map { $0.messages.first } + } else { + adMessage = .single(nil) + } + self.peerDisposable.set(combineLatest( queue: Queue.mainQueue(), peerView.get(), @@ -5503,10 +5569,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, - managingBot - ).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot in + managingBot, + adMessage + ).startStrict(next: { [weak self] peerView, globalNotificationSettings, onlineMemberCount, hasScheduledMessages, peerReportNotice, pinnedCount, threadInfo, hasSearchTags, hasSavedChats, isPremiumRequiredForMessaging, managingBot, adMessage in if let strongSelf = self { - if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging && managingBot == strongSelf.presentationInterfaceState.contactStatus?.managingBot { + if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages && strongSelf.threadInfo == threadInfo && strongSelf.presentationInterfaceState.hasSearchTags == hasSearchTags && strongSelf.presentationInterfaceState.hasSavedChats == hasSavedChats && strongSelf.presentationInterfaceState.isPremiumRequiredForMessaging == isPremiumRequiredForMessaging && managingBot == strongSelf.presentationInterfaceState.contactStatus?.managingBot && adMessage?.id == strongSelf.presentationInterfaceState.adMessage?.id { return } @@ -5791,51 +5858,72 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G boostsToUnrestrict = cachedChannelData.boostsToUnrestrict } + var adMessage = adMessage + if let peer = peerView.peers[peerView.peerId] as? TelegramUser, peer.botInfo != nil { + } else { + adMessage = nil + } + + if strongSelf.presentationInterfaceState.adMessage?.id != adMessage?.id { + animated = true + } + strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { return $0.updatedPeer { _ in return renderedPeer - }.updatedIsNotAccessible(isNotAccessible).updatedContactStatus(contactStatus).updatedHasBots(hasBots).updatedHasBotCommands(hasBotCommands).updatedBotMenuButton(botMenuButton).updatedIsArchived(isArchived).updatedPeerIsMuted(peerIsMuted).updatedPeerDiscussionId(peerDiscussionId).updatedPeerGeoLocation(peerGeoLocation).updatedExplicitelyCanPinMessages(explicitelyCanPinMessages).updatedHasScheduledMessages(hasScheduledMessages) - .updatedAutoremoveTimeout(autoremoveTimeout) - .updatedCurrentSendAsPeerId(currentSendAsPeerId) - .updatedCopyProtectionEnabled(copyProtectionEnabled) - .updatedHasSearchTags(hasSearchTags) - .updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging) - .updatedHasSavedChats(hasSavedChats) - .updatedAppliedBoosts(appliedBoosts) - .updatedBoostsToUnrestrict(boostsToUnrestrict) - .updatedHasBirthdayToday(hasBirthdayToday) - .updatedBusinessIntro(businessIntro) - .updatedInterfaceState { interfaceState in - var interfaceState = interfaceState - - if let channel = renderedPeer?.peer as? TelegramChannel { - if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if channel.hasBannedPermission(.banSendVoice) != nil { - if channel.hasBannedPermission(.banSendInstantVideos) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { - if channel.hasBannedPermission(.banSendVoice) == nil { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } - } - } else if let group = renderedPeer?.peer as? TelegramGroup { - if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } else if group.hasBannedPermission(.banSendVoice) { - if !group.hasBannedPermission(.banSendInstantVideos) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) - } - } else if group.hasBannedPermission(.banSendInstantVideos) { - if !group.hasBannedPermission(.banSendVoice) { - interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) - } - } - } - - return interfaceState - } + }.updatedIsNotAccessible(isNotAccessible) + .updatedContactStatus(contactStatus) + .updatedHasBots(hasBots) + .updatedHasBotCommands(hasBotCommands) + .updatedBotMenuButton(botMenuButton) + .updatedIsArchived(isArchived) + .updatedPeerIsMuted(peerIsMuted) + .updatedPeerDiscussionId(peerDiscussionId) + .updatedPeerGeoLocation(peerGeoLocation) + .updatedExplicitelyCanPinMessages(explicitelyCanPinMessages) + .updatedHasScheduledMessages(hasScheduledMessages) + .updatedAutoremoveTimeout(autoremoveTimeout) + .updatedCurrentSendAsPeerId(currentSendAsPeerId) + .updatedCopyProtectionEnabled(copyProtectionEnabled) + .updatedHasSearchTags(hasSearchTags) + .updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging) + .updatedHasSavedChats(hasSavedChats) + .updatedAppliedBoosts(appliedBoosts) + .updatedBoostsToUnrestrict(boostsToUnrestrict) + .updatedHasBirthdayToday(hasBirthdayToday) + .updatedBusinessIntro(businessIntro) + .updatedAdMessage(adMessage) + .updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + + if let channel = renderedPeer?.peer as? TelegramChannel { + if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if channel.hasBannedPermission(.banSendVoice) != nil { + if channel.hasBannedPermission(.banSendInstantVideos) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if channel.hasBannedPermission(.banSendInstantVideos) != nil { + if channel.hasBannedPermission(.banSendVoice) == nil { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } else if let group = renderedPeer?.peer as? TelegramGroup { + if group.hasBannedPermission(.banSendVoice) && group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } else if group.hasBannedPermission(.banSendVoice) { + if !group.hasBannedPermission(.banSendInstantVideos) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.video) + } + } else if group.hasBannedPermission(.banSendInstantVideos) { + if !group.hasBannedPermission(.banSendVoice) { + interfaceState = interfaceState.withUpdatedMediaRecordingMode(.audio) + } + } + } + + return interfaceState + } }) if case .standard(.default) = mode, let channel = renderedPeer?.chatMainPeer as? TelegramChannel, case .broadcast = channel.info { @@ -7133,6 +7221,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.recorderDataDisposable.dispose() self.displaySendWhenOnlineTipDisposable.dispose() self.networkSpeedEventsDisposable?.dispose() + self.postedScheduledMessagesEventsDisposable?.dispose() } deallocate() } @@ -8458,7 +8547,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let attributedText = chatInputStateStringWithAppliedEntities(text, entities: entities) var state = state - if let editMessageState = state.editMessageState, case let .media(options) = editMessageState.content, !options.isEmpty { + if let editMessageState = state.editMessageState { state = state.updatedEditMessageState(ChatEditInterfaceMessageState(content: editMessageState.content, mediaReference: mediaReference)) } if !text.isEmpty { @@ -9073,6 +9162,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + func shouldDivertMessagesToScheduled(targetPeer: EnginePeer? = nil, messages: [EnqueueMessage]) -> Signal { + return .single(false) + } + func sendMessages(_ messages: [EnqueueMessage], media: Bool = false, commit: Bool = false) { if case let .customChatContents(customChatContents) = self.subject { customChatContents.enqueueMessages(messages: messages) @@ -9083,37 +9176,78 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - var isScheduledMessages = false - if case .scheduledMessages = self.presentationInterfaceState.subject { - isScheduledMessages = true - } - - if commit || !isScheduledMessages { - self.commitPurposefulAction() + let _ = (self.shouldDivertMessagesToScheduled(messages: messages) + |> deliverOnMainQueue).startStandalone(next: { [weak self] shouldDivert in + guard let self else { + return + } - let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in - if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + var messages = messages + var shouldOpenScheduledMessages = false + + if shouldDivert { + messages = messages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes + } } - }) + shouldOpenScheduledMessages = true + } - donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) + var isScheduledMessages = false + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } - self.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) - } else { - self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time in - if let strongSelf = self { - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true) + if commit || !isScheduledMessages { + self.commitPurposefulAction() + + let _ = (enqueueMessages(account: self.context.account, peerId: peerId, messages: self.transformEnqueueMessages(messages)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + if let strongSelf = self, strongSelf.presentationInterfaceState.subject != .scheduledMessages { + strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() + } + }) + + donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) + + self.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) + + if !isScheduledMessages && shouldOpenScheduledMessages { + if let layoutActionOnViewTransitionAction = self.layoutActionOnViewTransitionAction { + self.layoutActionOnViewTransitionAction = nil + layoutActionOnViewTransitionAction() + } + + self.openScheduledMessages(force: true, completion: { _ in + }) } - }) - } + } else { + self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time in + if let strongSelf = self { + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time), commit: true) + } + }) + } + }) } func enqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!) |> deliverOnMainQueue).startStrict(next: { [weak self] items in - if let strongSelf = self { + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.shouldDivertMessagesToScheduled(messages: items.map(\.message)) + |> deliverOnMainQueue).startStandalone(next: { shouldDivert in + guard let strongSelf = self else { + return + } + var completionImpl: (() -> Void)? = completion var usedCorrelationId: Int64? @@ -9125,6 +9259,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var skipAddingTransitions = false + if shouldDivert { + skipAddingTransitions = true + } + for item in items { var message = item.message if message.groupingKey != nil { @@ -9248,7 +9386,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = scheduleTime { completion() } - } + }) })) } @@ -9530,13 +9668,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.resolvePeerByNameDisposable?.set((resolveSignal |> deliverOnMainQueue).start(next: { [weak self] peer in if let self, !hashtag.isEmpty { + if let _ = peerName, peer == nil { + self.present(textAlertController(context: self.context, title: nil, text: self.presentationInterfaceState.strings.Resolve_ChannelErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: self.presentationInterfaceState.strings.Common_OK, action: {})]), in: .window(.root)) + return + } var publicPosts = false if let peer = self.presentationInterfaceState.renderedPeer, let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info, !(channel.addressName ?? "").isEmpty { publicPosts = true } else if case let .customChatContents(contents) = self.subject, case let .hashTagSearch(publicPostsValue) = contents.kind { publicPosts = publicPostsValue } - let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, publicPosts: publicPosts) + let searchController = HashtagSearchController(context: self.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, mode: peerName != nil ? .chatOnly : .generic, publicPosts: peerName == nil && publicPosts) self.effectiveNavigationController?.pushViewController(searchController) } })) @@ -9658,7 +9800,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { urlContext = .generic } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: urlContext, navigationController: self.effectiveNavigationController, forceExternal: forceExternal, openPeer: { [weak self] peerId, navigation in + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: urlContext, navigationController: self.effectiveNavigationController, forceExternal: forceExternal, forceUpdate: false, openPeer: { [weak self] peerId, navigation in guard let strongSelf = self else { return } @@ -9754,7 +9896,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, contentContext: nil, progress: progress, completion: nil) } - func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, allowInlineWebpageResolution: Bool = false, progress: Promise? = nil, commit: @escaping () -> Void = {}) { + func openUrl( + _ url: String, + concealed: Bool, + forceExternal: Bool = false, + forceUpdate: Bool = false, + skipUrlAuth: Bool = false, + skipConcealedAlert: Bool = false, + message: Message? = nil, + allowInlineWebpageResolution: Bool = false, + progress: Promise? = nil, + commit: @escaping () -> Void = {} + ) { self.commitPurposefulAction() if allowInlineWebpageResolution, let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url { @@ -10180,6 +10333,60 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + func displayProcessingVideoTooltip(messageId: EngineMessage.Id) { + self.checksTooltipController?.dismiss() + + var latestNode: ChatMessageItemView? + self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + var found = false + for (message, _) in item.content { + if message.id == messageId { + found = true + break + } + } + if !found { + return + } + latestNode = itemNode + } + } + + if let itemNode = latestNode, let statusNode = itemNode.getStatusNode() { + let bounds = statusNode.view.convert(statusNode.view.bounds, to: self.chatDisplayNode.view) + let location = CGPoint(x: bounds.midX, y: bounds.minY - 8.0) + + let tooltipController = TooltipController(content: .text(self.presentationData.strings.Chat_MessageTooltipVideoProcessing), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, balancedTextLayout: true, isBlurred: true, timeout: 4.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + self.checksTooltipController = tooltipController + tooltipController.dismissed = { [weak self, weak tooltipController] _ in + if let strongSelf = self, let tooltipController = tooltipController, strongSelf.checksTooltipController === tooltipController { + strongSelf.checksTooltipController = nil + } + } + + let _ = self.chatDisplayNode.messageTransitionNode.addCustomOffsetHandler(itemNode: itemNode, update: { [weak tooltipController] offset, transition in + guard let tooltipController, tooltipController.isNodeLoaded else { + return false + } + guard let containerView = tooltipController.view else { + return false + } + containerView.bounds = containerView.bounds.offsetBy(dx: 0.0, dy: -offset) + transition.animateOffsetAdditive(layer: containerView.layer, offset: offset) + + return true + }) + + self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + guard let self else { + return nil + } + return (self.chatDisplayNode, CGRect(origin: location, size: CGSize())) + }, sourceRectIsGlobal: true)) + } + } + func dismissAllTooltips() { self.emojiTooltipController?.dismiss() self.sendingOptionsTooltipController?.dismiss() @@ -10343,8 +10550,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - func openScheduledMessages() { - guard let navigationController = self.effectiveNavigationController, navigationController.topViewController == self else { + func openScheduledMessages(force: Bool = false, completion: @escaping (ChatControllerImpl) -> Void = { _ in }) { + guard let navigationController = self.effectiveNavigationController else { + return + } + if navigationController.topViewController == self || force { + } else { return } @@ -10355,7 +10566,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = ChatControllerImpl(context: self.context, chatLocation: mappedChatLocation, subject: .scheduledMessages) controller.navigationPresentation = .modal - navigationController.pushViewController(controller) + navigationController.pushViewController(controller, completion: { [weak controller] in + let _ = controller + /*if let controller { + completion(controller) + }*/ + }) + completion(controller) } func openPinnedMessages(at messageId: MessageId?) { @@ -10803,4 +11020,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return true } } + + public var contentContainerNode: ASDisplayNode { + return self.chatDisplayNode.contentContainerNode + } } diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 5157826e77..42fb76fdd3 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -122,6 +122,9 @@ extension ChatControllerImpl { }) let commit: ([EnqueueMessage]) -> Void = { result in + guard let strongSelf = self else { + return + } var result = result strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }).updatedSearch(nil) }) @@ -133,96 +136,128 @@ extension ChatControllerImpl { result[i] = result[i].withUpdatedCorrelationId(correlationId) } - var displayPeers: [EnginePeer] = [] - for peer in peers { - let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: result) - |> deliverOnMainQueue).startStandalone(next: { messageIds in - if let strongSelf = self { - let signals: [Signal] = messageIds.compactMap({ id -> Signal? in - guard let id = id else { - return nil - } - return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) - |> mapToSignal { status, _ -> Signal in - if status != nil { - return .never() - } else { - return .single(true) - } - } - |> take(1) - }) - if strongSelf.shareStatusDisposable == nil { - strongSelf.shareStatusDisposable = MetaDisposable() - } - strongSelf.shareStatusDisposable?.set((combineLatest(signals) - |> deliverOnMainQueue).startStrict()) - } - }) - - if case let .secretChat(secretPeer) = peer { - if let peer = peerMap[secretPeer.regularPeerId] { - displayPeers.append(peer) - } - } else { - displayPeers.append(peer) + let targetPeersShouldDivertSignals: [Signal<(EnginePeer, Bool), NoError>] = peers.map { peer -> Signal<(EnginePeer, Bool), NoError> in + return strongSelf.shouldDivertMessagesToScheduled(targetPeer: peer, messages: result) + |> map { shouldDivert -> (EnginePeer, Bool) in + return (peer, shouldDivert) } } - - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let text: String - var savedMessages = false - if displayPeers.count == 1, let peerId = displayPeers.first?.id, peerId == strongSelf.context.account.peerId { - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many - savedMessages = true - } else { - if displayPeers.count == 1, let peer = displayPeers.first { - var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string - } else if displayPeers.count == 2, let firstPeer = displayPeers.first, let secondPeer = displayPeers.last { - var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") - var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string - } else if let peer = displayPeers.first { - var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - peerName = peerName.replacingOccurrences(of: "**", with: "") - text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(displayPeers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(displayPeers.count - 1)").string - } else { - text = "" - } - } - - let reactionItems: Signal<[ReactionItem], NoError> - if savedMessages && messages.count > 0 { - reactionItems = tagMessageReactions(context: strongSelf.context, subPeerId: nil) - } else { - reactionItems = .single([]) - } - - let _ = (reactionItems - |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] reactionItems in - guard let strongSelf else { + let targetPeersShouldDivert: Signal<[(EnginePeer, Bool)], NoError> = combineLatest(targetPeersShouldDivertSignals) + let _ = (targetPeersShouldDivert + |> deliverOnMainQueue).startStandalone(next: { targetPeersShouldDivert in + guard let strongSelf = self else { return } - strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages && messages.count > 0 ? .top : .bottom, animateInAsReplacement: true, action: { action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return + var displayConvertingTooltip = false + + var displayPeers: [EnginePeer] = [] + for (peer, shouldDivert) in targetPeersShouldDivert { + var peerMessages = result + if shouldDivert { + displayConvertingTooltip = true + peerMessages = peerMessages.map { message -> EnqueueMessage in + return message.withUpdatedAttributes { attributes in + var attributes = attributes + attributes.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute }) + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(Date().timeIntervalSince1970) + 10 * 24 * 60 * 60)) + return attributes } - guard let navigationController = self.navigationController as? NavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) - }) + } } - return false - }, additionalView: (savedMessages && messages.count > 0) ? chatShareToSavedMessagesAdditionalView(strongSelf, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) + + let _ = (enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: peerMessages) + |> deliverOnMainQueue).startStandalone(next: { messageIds in + if let strongSelf = self { + let signals: [Signal] = messageIds.compactMap({ id -> Signal? in + guard let id = id else { + return nil + } + return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + }) + if strongSelf.shareStatusDisposable == nil { + strongSelf.shareStatusDisposable = MetaDisposable() + } + strongSelf.shareStatusDisposable?.set((combineLatest(signals) + |> deliverOnMainQueue).startStrict()) + } + }) + + if case let .secretChat(secretPeer) = peer { + if let peer = peerMap[secretPeer.regularPeerId] { + displayPeers.append(peer) + } + } else { + displayPeers.append(peer) + } + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let text: String + var savedMessages = false + if displayPeers.count == 1, let peerId = displayPeers.first?.id, peerId == strongSelf.context.account.peerId { + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many + savedMessages = true + } else { + if displayPeers.count == 1, let peer = displayPeers.first { + var peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string : presentationData.strings.Conversation_ForwardTooltip_Chat_Many(peerName).string + } else if displayPeers.count == 2, let firstPeer = displayPeers.first, let secondPeer = displayPeers.last { + var firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + firstPeerName = firstPeerName.replacingOccurrences(of: "**", with: "") + var secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + secondPeerName = secondPeerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string : presentationData.strings.Conversation_ForwardTooltip_TwoChats_Many(firstPeerName, secondPeerName).string + } else if let peer = displayPeers.first { + var peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerName = peerName.replacingOccurrences(of: "**", with: "") + text = messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(displayPeers.count - 1)").string : presentationData.strings.Conversation_ForwardTooltip_ManyChats_Many(peerName, "\(displayPeers.count - 1)").string + } else { + text = "" + } + } + + let reactionItems: Signal<[ReactionItem], NoError> + if savedMessages && messages.count > 0 { + reactionItems = tagMessageReactions(context: strongSelf.context, subPeerId: nil) + } else { + reactionItems = .single([]) + } + + let _ = (reactionItems + |> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] reactionItems in + guard let strongSelf else { + return + } + + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, position: savedMessages && messages.count > 0 ? .top : .bottom, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }, additionalView: (savedMessages && messages.count > 0) ? chatShareToSavedMessagesAdditionalView(strongSelf, reactionItems: reactionItems, correlationIds: correlationIds) : nil), in: .current) + }) + + if displayConvertingTooltip { + } }) } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index d461f2a2d1..e8b43c3088 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -193,6 +193,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private var chatImportStatusPanel: ChatImportStatusPanel? + private(set) var adPanelNode: ChatAdPanelNode? + private let titleAccessoryPanelContainer: ChatControllerTitlePanelNodeContainer private var titleAccessoryPanelNode: ChatTitleAccessoryPanelNode? @@ -1427,6 +1429,50 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.chatImportStatusPanel = nil } + var dismissedAdPanelNode: ChatAdPanelNode? + var adPanelHeight: CGFloat? + + var displayAdPanel = false + if let _ = self.chatPresentationInterfaceState.adMessage { + if let chatHistoryState = self.chatPresentationInterfaceState.chatHistoryState, case .loaded(false, _) = chatHistoryState { + if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, user.botInfo != nil && !self.chatPresentationInterfaceState.peerIsBlocked && self.chatPresentationInterfaceState.hasAtLeast3Messages { + displayAdPanel = true + } + } + } + + if displayAdPanel { + var animateAppearance = false + let adPanelNode: ChatAdPanelNode + if let current = self.adPanelNode { + adPanelNode = current + } else { + adPanelNode = ChatAdPanelNode(context: self.context, animationCache: self.controllerInteraction.presentationContext.animationCache, animationRenderer: self.controllerInteraction.presentationContext.animationRenderer) + adPanelNode.controllerInteraction = self.controllerInteraction + adPanelNode.clipsToBounds = true + animateAppearance = true + } + + if self.adPanelNode != adPanelNode { + dismissedAdPanelNode = self.adPanelNode + self.adPanelNode = adPanelNode + self.titleAccessoryPanelContainer.addSubnode(adPanelNode) + + adPanelNode.clipsToBounds = true + } + + let height = adPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) + adPanelHeight = height + if transition.isAnimated && animateAppearance { + adPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + adPanelNode.subnodeTransform = CATransform3DMakeTranslation(0.0, -height, 0.0) + transition.updateSublayerTransformOffset(layer: adPanelNode.layer, offset: CGPoint()) + } + } else if let adPanelNode = self.adPanelNode { + dismissedAdPanelNode = adPanelNode + self.adPanelNode = nil + } + var inputPanelNodeBaseHeight: CGFloat = 0.0 if let inputPanelNode = self.inputPanelNode { inputPanelNodeBaseHeight += inputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) @@ -1726,14 +1772,21 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { extraNavigationBarHeight += panelHeight } - updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraTransition) - var importStatusPanelFrame: CGRect? if let _ = self.chatImportStatusPanel, let panelHeight = importStatusPanelHeight { importStatusPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: panelHeight)) insets.top += panelHeight } + var adPanelFrame: CGRect? + if let _ = self.adPanelNode, let panelHeight = adPanelHeight { + adPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: extraNavigationBarHeight), size: CGSize(width: layout.size.width, height: panelHeight)) + insets.top += panelHeight + extraNavigationBarHeight += panelHeight + } + + updateExtraNavigationBarBackgroundHeight(extraNavigationBarHeight, extraNavigationBarHitTestSlop, extraTransition) + let contentBounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) if let backgroundEffectNode = self.backgroundEffectNode { @@ -2164,21 +2217,25 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) self.navigateButtons.update(rect: apparentNavigateButtonsFrame, within: layout.size, transition: transition) - if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame = titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { + if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { titleAccessoryPanelNode.frame = titleAccessoryPanelFrame transition.animatePositionAdditive(node: titleAccessoryPanelNode, offset: CGPoint(x: 0.0, y: -titleAccessoryPanelFrame.height)) } - if let chatTranslationPanel = self.chatTranslationPanel, let translationPanelFrame = translationPanelFrame, !chatTranslationPanel.frame.equalTo(translationPanelFrame) { + if let chatTranslationPanel = self.chatTranslationPanel, let translationPanelFrame, !chatTranslationPanel.frame.equalTo(translationPanelFrame) { chatTranslationPanel.frame = translationPanelFrame transition.animatePositionAdditive(node: chatTranslationPanel, offset: CGPoint(x: 0.0, y: -translationPanelFrame.height)) } - if let chatImportStatusPanel = self.chatImportStatusPanel, let importStatusPanelFrame = importStatusPanelFrame, !chatImportStatusPanel.frame.equalTo(importStatusPanelFrame) { + if let chatImportStatusPanel = self.chatImportStatusPanel, let importStatusPanelFrame, !chatImportStatusPanel.frame.equalTo(importStatusPanelFrame) { chatImportStatusPanel.frame = importStatusPanelFrame //transition.animatePositionAdditive(node: chatImportStatusPanel, offset: CGPoint(x: 0.0, y: -titleAccessoryPanelFrame.height)) } + if let adPanelNode = self.adPanelNode, let adPanelFrame, !adPanelNode.frame.equalTo(adPanelFrame) { + adPanelNode.frame = adPanelFrame + } + if let secondaryInputPanelNode = self.secondaryInputPanelNode, let apparentSecondaryInputPanelFrame = apparentSecondaryInputPanelFrame, !secondaryInputPanelNode.frame.equalTo(apparentSecondaryInputPanelFrame) { if immediatelyLayoutSecondaryInputPanelAndAnimateAppearance { secondaryInputPanelNode.frame = apparentSecondaryInputPanelFrame.offsetBy(dx: 0.0, dy: apparentSecondaryInputPanelFrame.height + previousInputPanelBackgroundFrame.maxY - apparentSecondaryInputPanelFrame.maxY) @@ -2270,7 +2327,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - if let dismissedTitleAccessoryPanelNode = dismissedTitleAccessoryPanelNode { + if let dismissedTitleAccessoryPanelNode { var dismissedPanelFrame = dismissedTitleAccessoryPanelNode.frame dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height transition.updateFrame(node: dismissedTitleAccessoryPanelNode, frame: dismissedPanelFrame, completion: { [weak dismissedTitleAccessoryPanelNode] _ in @@ -2278,7 +2335,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { }) } - if let dismissedTranslationPanelNode = dismissedTranslationPanelNode { + if let dismissedTranslationPanelNode { var dismissedPanelFrame = dismissedTranslationPanelNode.frame dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height transition.updateAlpha(node: dismissedTranslationPanelNode, alpha: 0.0, completion: { [weak dismissedTranslationPanelNode] _ in @@ -2287,7 +2344,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { dismissedTranslationPanelNode.animateOut() } - if let dismissedImportStatusPanelNode = dismissedImportStatusPanelNode { + if let dismissedImportStatusPanelNode { var dismissedPanelFrame = dismissedImportStatusPanelNode.frame dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height transition.updateFrame(node: dismissedImportStatusPanelNode, frame: dismissedPanelFrame, completion: { [weak dismissedImportStatusPanelNode] _ in @@ -2295,6 +2352,15 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { }) } + if let dismissedAdPanelNode { + var dismissedPanelFrame = dismissedAdPanelNode.frame + dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height + transition.updateAlpha(node: dismissedAdPanelNode, alpha: 0.0) + transition.updateFrame(node: dismissedAdPanelNode, frame: dismissedPanelFrame, completion: { [weak dismissedAdPanelNode] _ in + dismissedAdPanelNode?.removeFromSupernode() + }) + } + if let inputPanelNode = self.inputPanelNode, let apparentInputPanelFrame = apparentInputPanelFrame, !inputPanelNode.frame.equalTo(apparentInputPanelFrame) { if immediatelyLayoutInputPanelAndAnimateAppearance { inputPanelNode.frame = apparentInputPanelFrame.offsetBy(dx: 0.0, dy: apparentInputPanelFrame.height + previousInputPanelBackgroundFrame.maxY - apparentInputBackgroundFrame.maxY) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 37d82d35fd..a248e12346 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -616,8 +616,8 @@ extension ChatControllerImpl { let params = WebAppParameters(source: fromAttachMenu ? .attachMenu : .generic, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: payload, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: false) let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, replyToMessageId: replyMessageSubject?.messageId, threadId: strongSelf.chatLocation.threadId) - controller.openUrl = { [weak self] url, concealed, commit in - self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) + controller.openUrl = { [weak self] url, concealed, forceUpdate, commit in + self?.openUrl(url, concealed: concealed, forceExternal: true, forceUpdate: forceUpdate, commit: commit) } controller.getNavigationController = { [weak self] in return self?.effectiveNavigationController @@ -711,7 +711,7 @@ extension ChatControllerImpl { }) } - func oldPresentAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { + func presentEditingAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -814,188 +814,184 @@ extension ChatControllerImpl { hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat } - let controller = legacyAttachmentMenu(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, editMediaOptions: menuEditMediaOptions, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, hasSchedule: hasSchedule, canSendPolls: canSendPolls, updatedPresentationData: strongSelf.updatedPresentationData, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, initialCaption: inputText, openGallery: { - self?.presentOldMediaPicker(fileMode: false, editingMedia: editMediaOptions != nil, completion: { signals, silentPosting, scheduleTime in + let controller = legacyAttachmentMenu( + context: strongSelf.context, + peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, + threadTitle: strongSelf.threadInfo?.title, chatLocation: strongSelf.chatLocation, + editMediaOptions: menuEditMediaOptions, + addingMedia: editMediaOptions == nil, + saveEditedPhotos: settings.storeEditedPhotos, + allowGrouping: true, + hasSchedule: hasSchedule, + canSendPolls: canSendPolls, + updatedPresentationData: strongSelf.updatedPresentationData, + parentController: legacyController, + recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, + initialCaption: inputText, + openGallery: { + self?.presentOldMediaPicker(fileMode: false, editingMedia: true, completion: { signals, silentPosting, scheduleTime in + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + self?.editMessageMediaWithLegacySignals(signals) + }) + }, openCamera: { [weak self] cameraView, menuController in + if let strongSelf = self { + var enablePhoto = true + var enableVideo = true + + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { + enableVideo = false + } + + var bannedSendPhotos: (Int32, Bool)? + var bannedSendVideos: (Int32, Bool)? + + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + if let channel = peer as? TelegramChannel { + if let value = channel.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = value + } + if let value = channel.hasBannedPermission(.banSendVideos) { + bannedSendVideos = value + } + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(.banSendPhotos) { + bannedSendPhotos = (Int32.max, false) + } + if group.hasBannedPermission(.banSendVideos) { + bannedSendVideos = (Int32.max, false) + } + } + } + + if bannedSendPhotos != nil { + enablePhoto = false + } + if bannedSendVideos != nil { + enableVideo = false + } + + var storeCapturedPhotos = false + var hasSchedule = false + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat + + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat + } + + presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in + if let strongSelf = self { + strongSelf.editMessageMediaWithLegacySignals(signals!) + + if !inputText.string.isEmpty { + strongSelf.clearInputText() + } + } + }, recognizedQRCode: { [weak self] code in + if let strongSelf = self { + if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) { + strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) + } + } + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView(isFile: false) + }) + } + }, openFileGallery: { + self?.presentFileMediaPickerOptions(editingMessage: true) + }, openWebSearch: { [weak self] in + self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }) + }, openMap: { + self?.presentLocationPicker() + }, openContacts: { + self?.presentContactPicker() + }, openPoll: { + if let controller = self?.configurePollCreation() { + self?.effectiveNavigationController?.pushViewController(controller) + } + }, presentSelectionLimitExceeded: { + guard let strongSelf = self else { + return + } + let text: String + if slowModeEnabled { + text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached + } else { + text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentCantSendMultipleFiles: { + guard let strongSelf = self else { + return + } + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, presentJpegConversionAlert: { completion in + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { + completion(false) + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { + completion(true) + })], actionLayout: .vertical), in: .window(.root)) + }, presentSchedulePicker: { [weak self] _, done in + if let strongSelf = self { + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in + if let strongSelf = self { + done(time) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + strongSelf.openScheduledMessages() + } + } + }) + } + }, presentTimerPicker: { [weak self] done in + if let strongSelf = self { + strongSelf.presentTimerPicker(style: .media, completion: { time in + done(time) + }) + } + }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in + guard let strongSelf = self else { + completion() + return + } if !inputText.string.isEmpty { strongSelf.clearInputText() } - if editMediaOptions != nil { - self?.editMessageMediaWithLegacySignals(signals) - } else { - self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - } - }) - }, openCamera: { [weak self] cameraView, menuController in - if let strongSelf = self { - var enablePhoto = true - var enableVideo = true - - if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { - enableVideo = false - } - - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - } - } - - if bannedSendPhotos != nil { - enablePhoto = false - } - if bannedSendVideos != nil { - enableVideo = false - } - - var storeCapturedPhotos = false - var hasSchedule = false - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { - storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat - - hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat - } - - presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in - if let strongSelf = self { - if editMediaOptions != nil { - strongSelf.editMessageMediaWithLegacySignals(signals!) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) - } - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - } - }, recognizedQRCode: { [weak self] code in - if let strongSelf = self { - if let (host, port, username, password, secret) = parseProxyUrl(sharedContext: strongSelf.context.sharedContext, url: code) { - strongSelf.openResolved(result: ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret), sourceMessageId: nil) - } - } - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView(isFile: false) - }) - } - }, openFileGallery: { - self?.presentFileMediaPickerOptions(editingMessage: editMediaOptions != nil) - }, openWebSearch: { [weak self] in - self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) - }, openMap: { - self?.presentLocationPicker() - }, openContacts: { - self?.presentContactPicker() - }, openPoll: { - if let controller = self?.configurePollCreation() { - self?.effectiveNavigationController?.pushViewController(controller) - } - }, presentSelectionLimitExceeded: { - guard let strongSelf = self else { - return - } - let text: String - if slowModeEnabled { - text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached - } else { - text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentCantSendMultipleFiles: { - guard let strongSelf = self else { - return - } - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Chat_AttachmentMultipleFilesDisabled, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, presentJpegConversionAlert: { completion in - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.MediaPicker_KeepHeic, action: { - completion(false) - }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.MediaPicker_ConvertToJpeg, action: { - completion(true) - })], actionLayout: .vertical), in: .window(.root)) - }, presentSchedulePicker: { [weak self] _, done in - if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time in - if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { - strongSelf.openScheduledMessages() - } - } - }) - } - }, presentTimerPicker: { [weak self] done in - if let strongSelf = self { - strongSelf.presentTimerPicker(style: .media, completion: { time in - done(time) - }) - } - }, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, getAnimatedTransitionSource, completion in - guard let strongSelf = self else { - completion() - return - } - if !inputText.string.isEmpty { - strongSelf.clearInputText() - } - if editMediaOptions != nil { strongSelf.editMessageMediaWithLegacySignals(signals!) completion() - } else { - let immediateCompletion = getAnimatedTransitionSource == nil - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: { - if !immediateCompletion { - completion() - } - }) - if immediateCompletion { - completion() - } - } - }, selectRecentlyUsedInlineBot: { [weak self] peer in - if let strongSelf = self, let addressName = peer.addressName { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in - return .text + }, selectRecentlyUsedInlineBot: { [weak self] peer in + if let strongSelf = self, let addressName = peer.addressName { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState({ $0.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "@" + addressName + " "))) }).updatedInputMode({ _ in + return .text + }) }) - }) + } + }, getCaptionPanelView: { [weak self] in + return self?.getCaptionPanelView(isFile: false) + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) } - }, getCaptionPanelView: { [weak self] in - return self?.getCaptionPanelView(isFile: false) - }, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) + ) controller.didDismiss = { [weak legacyController] _ in legacyController?.dismiss() } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 7ca0a4a401..ee04acb0df 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -192,7 +192,7 @@ extension ChatControllerImpl { if canViewMessageReactionList(message: message) { items = ContextController.Items(content: .custom(ReactionListContextMenuContent( context: self.context, - displayReadTimestamps: false, + displayReadTimestamps: true, availableReactions: availableReactions, animationCache: self.controllerInteraction!.presentationContext.animationCache, animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer, @@ -513,7 +513,7 @@ extension ChatControllerImpl { title = self.presentationData.strings.Chat_ToastStarsSent_Title(Int32(self.currentSendStarsUndoCount)) } - let textItems = extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ + let textItems = AnimatedTextComponent.extractAnimatedTextString(string: self.presentationData.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ 0: .number(self.currentSendStarsUndoCount, minDigits: 1), 1: .text(self.presentationData.strings.Chat_ToastStarsSent_TextStarAmount(Int32(self.currentSendStarsUndoCount))) ]) @@ -536,31 +536,3 @@ extension ChatControllerImpl { } } } - -private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { - var textItems: [AnimatedTextComponent.Item] = [] - - var previousIndex = 0 - let nsString = string.string as NSString - for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { - if range.range.lowerBound > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) - } - if let value = mapping[range.index] { - let isUnbreakable: Bool - switch value { - case .text: - isUnbreakable = true - case .number: - isUnbreakable = false - } - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) - } - previousIndex = range.range.upperBound - } - if nsString.length > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) - } - - return textItems -} diff --git a/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift b/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift index 59f3755db3..07cd1d573d 100644 --- a/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift +++ b/submodules/TelegramUI/Sources/ChatControllerUpdateSearch.swift @@ -77,6 +77,7 @@ extension ChatControllerImpl { if queryIsEmpty { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) self.searchResult.set(.single(nil)) if let data = interfaceState.search { @@ -104,6 +105,7 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } + strongSelf.searchResultsCount.set(results.totalCount) var navigateIndex: MessageIndex? strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search { @@ -152,6 +154,7 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } + strongSelf.searchResultsCount.set(results.totalCount) strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in if let data = current.search, let previousResultsState = data.resultsState { let messageIndices = results.messages.map({ $0.index }).sorted() @@ -167,11 +170,13 @@ extension ChatControllerImpl { })) } else { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) } } } else { self.searching.set(false) + self.searchResultsCount.set(0) self.searchDisposable?.set(nil) if let data = interfaceState.search { diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 7b756fe096..bff0145832 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -223,23 +223,6 @@ func chatHistoryEntriesForView( } if groupMessages || reverseGroupedMessages { - /*if !groupBucket.isEmpty && message.groupInfo != groupBucket[0].0.groupInfo { - if reverseGroupedMessages { - groupBucket.reverse() - } - if groupMessages { - let groupStableId = groupBucket[0].0.groupInfo!.stableId - if !existingGroupStableIds.contains(groupStableId) { - existingGroupStableIds.append(groupStableId) - entries.append(.MessageGroupEntry(groupBucket[0].0.groupInfo!, groupBucket, presentationData)) - } - } else { - for (message, isRead, selection, attributes, location) in groupBucket { - entries.append(.MessageEntry(message, presentationData, isRead, location, selection, attributes)) - } - } - groupBucket.removeAll() - }*/ if let messageGroupingKey = message.groupingKey, (groupMessages || reverseGroupedMessages) { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { @@ -286,14 +269,6 @@ func chatHistoryEntriesForView( if !found { entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, attributes)) } - - /*let selection: ChatHistoryMessageSelection - if let selectedMessages = selectedMessages { - selection = .selectable(selected: selectedMessages.contains(message.id)) - } else { - selection = .none - } - groupBucket.append((message, isRead, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }), entry.location))*/ } else { let selection: ChatHistoryMessageSelection if let selectedMessages = selectedMessages { @@ -316,27 +291,63 @@ func chatHistoryEntriesForView( } else { selection = .none } + entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }))) } } - - /*if !groupBucket.isEmpty { - assert(groupMessages || reverseGroupedMessages) - if reverseGroupedMessages { - groupBucket.reverse() - } - if groupMessages { - let groupStableId = groupBucket[0].0.groupInfo!.stableId - if !existingGroupStableIds.contains(groupStableId) { - existingGroupStableIds.append(groupStableId) - entries.append(.MessageGroupEntry(groupBucket[0].0.groupInfo!, groupBucket, presentationData)) + + let insertPendingProcessingMessage: ([Message], Int) -> Void = { messages, index in + let serviceMessage = Message( + stableId: UInt32.max - messages[0].stableId, + stableVersion: 0, + id: MessageId(peerId: messages[0].id.peerId, namespace: -1, id: messages[0].id.id), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: messages[0].timestamp, + flags: [.Incoming], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: nil, + text: "", + attributes: [], + media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Chat_VideoProcessingServiceMessage(Int32(messages.count)), entities: [], additionalAttributes: nil))], + peers: SimpleDictionary(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + entries.insert(.MessageEntry(serviceMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: index) + } + + for i in (0 ..< entries.count).reversed() { + switch entries[i] { + case let .MessageEntry(message, _, _, _, _, _): + if message.id.namespace == Namespaces.Message.ScheduledCloud && message.pendingProcessingAttribute != nil { + insertPendingProcessingMessage([message], i) } - } else { - for (message, isRead, selection, attributes, location) in groupBucket { - entries.append(.MessageEntry(message, presentationData, isRead, location, selection, attributes)) + case let .MessageGroupEntry(_, messages, _): + if !messages.isEmpty && messages[0].0.id.namespace == Namespaces.Message.ScheduledCloud { + var videoCount = 0 + for message in messages { + if message.0.pendingProcessingAttribute != nil { + videoCount += 1 + } + } + if videoCount != 0 { + insertPendingProcessingMessage(messages.map(\.0), i) + } } + default: + break } - }*/ + } if let lowerTimestamp = view.entries.last?.message.timestamp, let upperTimestamp = view.entries.first?.message.timestamp { if let joinMessage { @@ -657,6 +668,9 @@ func chatHistoryEntriesForView( if reverse { return (entries.reversed(), currentState) } else { + #if DEBUG + assert(entries.map(\.stableId) == entries.sorted().map(\.stableId)) + #endif return (entries, currentState) } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 502b9a9c3a..106debf3c2 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -351,8 +351,7 @@ private func extractAssociatedData( chatThemes: [TelegramTheme], deviceContactsNumbers: Set, isInline: Bool, - showSensitiveContent: Bool, - starGifts: [Int64 : TelegramMediaFile] + showSensitiveContent: Bool ) -> ChatMessageItemAssociatedData { var automaticDownloadPeerId: EnginePeer.Id? var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel @@ -407,7 +406,7 @@ private func extractAssociatedData( automaticDownloadPeerId = message.peerId } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, starGifts: starGifts) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent) } private extension ChatHistoryLocationInput { @@ -654,6 +653,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto private var loadStateUpdated: ((ChatHistoryNodeLoadState, Bool) -> Void)? private var additionalLoadStateUpdated: [(ChatHistoryNodeLoadState, Bool) -> Void] = [] + public private(set) var hasAtLeast3Messages: Bool = false + public var hasAtLeast3MessagesUpdated: ((Bool) -> Void)? + public private(set) var hasPlentyOfMessages: Bool = false public var hasPlentyOfMessagesUpdated: ((Bool) -> Void)? @@ -779,7 +781,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if case .bubbles = mode, let peerId = displayAdPeer { let adMessagesContext = context.engine.messages.adMessages(peerId: peerId) self.adMessagesContext = adMessagesContext - adMessages = adMessagesContext.state + if peerId.namespace == Namespaces.Peer.CloudUser { + adMessages = .single((nil, [])) + } else { + adMessages = adMessagesContext.state + } } else { self.adMessagesContext = nil adMessages = .single((nil, [])) @@ -1611,17 +1617,6 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } |> distinctUntilChanged - let starGifts: Signal<[Int64 : TelegramMediaFile], NoError> = context.engine.payments.cachedStarGifts() - |> map { gifts in - var files: [Int64 : TelegramMediaFile] = [:] - if let gifts { - for gift in gifts { - files[gift.id] = gift.file - } - } - return files - } - let messageViewQueue = Queue.mainQueue() let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, historyViewUpdate, @@ -1648,9 +1643,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto audioTranscriptionTrial, chatThemes, deviceContactsNumbers, - contentSettings, - starGifts - ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings, starGifts in + contentSettings + ).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings in let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises func applyHole() { @@ -1861,15 +1855,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto if languageCode.hasSuffix(rawSuffix) { languageCode = String(languageCode.dropLast(rawSuffix.count)) } - if languageCode == "nb" { - languageCode = "no" - } else if languageCode == "pt-br" { - languageCode = "pt" - } - translateToLanguage = languageCode + translateToLanguage = normalizeTranslationLanguage(languageCode) } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"), starGifts: starGifts) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive")) var includeEmbeddedSavedChatInfo = false if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated { @@ -3481,6 +3470,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } for id in self.context.engine.messages.synchronouslyIsMessageDeletedInteractively(ids: testIds) { + if id.namespace == Namespaces.Message.ScheduledCloud { + continue + } inner: for (stableId, listId) in maybeRemovedInteractivelyMessageIds { if listId == id { expiredMessageStableIds.insert(stableId) @@ -3790,13 +3782,18 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } + var hasAtLeast3Messages = false var hasPlentyOfMessages = false var hasLotsOfMessages = false if let historyView = strongSelf.historyView { if historyView.originalView.holeEarlier || historyView.originalView.holeLater { + hasAtLeast3Messages = true hasPlentyOfMessages = true hasLotsOfMessages = true } else if !historyView.originalView.holeEarlier && !historyView.originalView.holeLater { + if historyView.filteredEntries.count >= 3 { + hasAtLeast3Messages = true + } if historyView.filteredEntries.count >= 10 { hasPlentyOfMessages = true } @@ -3806,6 +3803,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } } + if strongSelf.hasAtLeast3Messages != hasAtLeast3Messages { + strongSelf.hasAtLeast3Messages = hasAtLeast3Messages + strongSelf.hasAtLeast3MessagesUpdated?(hasAtLeast3Messages) + } if strongSelf.hasPlentyOfMessages != hasPlentyOfMessages { strongSelf.hasPlentyOfMessages = hasPlentyOfMessages strongSelf.hasPlentyOfMessagesUpdated?(hasPlentyOfMessages) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index b09109858c..11bf2eabe9 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -10,7 +10,7 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult switch result { case let .stickers(items): return (0, !items.isEmpty) - case let .hashtags(items): + case let .hashtags(items, _): return (1, !items.isEmpty) case let .mentions(items): return (2, !items.isEmpty) @@ -98,15 +98,19 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa return panel } } - case let .hashtags(results): - if !results.isEmpty { + case let .hashtags(results, query): + var peer: EnginePeer? + if let chatPeer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, chatPeer.addressName != nil { + peer = EnginePeer(chatPeer) + } + if !results.isEmpty || (peer != nil && query.count >= 4) { if let currentPanel = currentPanel as? HashtagChatInputContextPanelNode { - currentPanel.updateResults(results) + currentPanel.updateResults(results, query: query, peer: peer) return currentPanel } else { let panel = HashtagChatInputContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize, chatPresentationContext: controllerInteraction.presentationContext) panel.interfaceInteraction = interfaceInteraction - panel.updateResults(results) + panel.updateResults(results, query: query, peer: peer) return panel } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 1ca2adc044..0a60d44b7f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -476,6 +476,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { return .single(ContextController.Items(content: .list([]))) } + if let message = messages.first, message.id.namespace < 0 { + return .single(ContextController.Items(content: .list([]))) + } var isEmbeddedMode = false if case .standard(.embedded) = chatPresentationInterfaceState.mode { @@ -547,7 +550,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor) }, iconSource: nil, action: { _, f in f(.dismissWithoutContent) - controllerInteraction.navigationController()?.pushViewController(AdsInfoScreen(context: context)) + controllerInteraction.navigationController()?.pushViewController(AdsInfoScreen(context: context, mode: .channel)) }))) actions.append(.action(ContextMenuActionItem(text: presentationData.strings.Chat_ContextMenu_ReportAd, textColor: .primary, textLayout: .twoLinesMax, textFont: .custom(font: Font.regular(presentationData.listsFontSize.baseDisplaySize - 1.0), height: nil, verticalOffset: nil), badge: nil, icon: { theme in @@ -1137,9 +1140,28 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if data.messageActions.options.contains(.sendScheduledNow) { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.ScheduledMessages_SendNow, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) - f(.dismissWithoutContent) + }, action: { c, _ in + if messages.contains(where: { $0.pendingProcessingAttribute != nil }) { + c?.dismiss(completion: { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + controllerInteraction.presentController(standardTextAlertController( + theme: AlertControllerTheme(presentationData: presentationData), + title: presentationData.strings.Chat_ScheduledForceSendProcessingVideo_Title, + text: presentationData.strings.Chat_ScheduledForceSendProcessingVideo_Text, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_ScheduledForceSendProcessingVideo_Action, action: { + controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ), nil) + }) + } else { + c?.dismiss(result: .dismissWithoutContent, completion: nil) + controllerInteraction.sendScheduledMessagesNow(selectAll ? messages.map { $0.id } : [message.id]) + } }))) } @@ -1857,6 +1879,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState reactionCount = 0 } } + + let isEdited = message.attributes.contains(where: { attribute in + if let attribute = attribute as? EditedMessageAttribute, !attribute.isHidden, attribute.date != 0 { + return true + } + return false + }) + + if isEdited { + if !actions.isEmpty { + actions.insert(.separator, at: 0) + } + actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, hasReadReports: false, isEdit: true, stats: MessageReadStats(reactionCount: 0, peers: [], readTimestamps: [:]), action: nil), false), at: 0) + } if let peer = message.peers[message.id.peerId], (canViewStats || reactionCount != 0) { var hasReadReports = false @@ -1880,18 +1916,18 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } else { reactionCount = 0 } - - /*var readStats = readStats - if !canViewStats { - readStats = MessageReadStats(reactionCount: 0, peers: []) - }*/ if hasReadReports || reactionCount != 0 { if !actions.isEmpty { actions.insert(.separator, at: 0) } + + var readStats = readStats + if !(hasReadReports || reactionCount != 0) { + readStats = MessageReadStats(reactionCount: 0, peers: [], readTimestamps: [:]) + } - actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, hasReadReports: hasReadReports, stats: readStats, action: { c, f, stats, customReactionEmojiPacks, firstCustomEmojiReaction in + actions.insert(.custom(ChatReadReportContextItem(context: context, message: message, hasReadReports: hasReadReports, isEdit: false, stats: readStats, action: { c, f, stats, customReactionEmojiPacks, firstCustomEmojiReaction in if message.id.peerId.namespace == Namespaces.Peer.CloudUser { if let stats, stats.peers.isEmpty { c.dismiss(completion: { @@ -2196,8 +2232,10 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } if id.namespace == Namespaces.Message.ScheduledCloud { optionsMap[id]!.insert(.sendScheduledNow) - if canEditMessage(accountPeerId: accountPeerId, limitsConfiguration: limitsConfiguration, message: message, reschedule: true) { - optionsMap[id]!.insert(.editScheduledTime) + if message.pendingProcessingAttribute == nil { + if canEditMessage(accountPeerId: accountPeerId, limitsConfiguration: limitsConfiguration, message: message, reschedule: true) { + optionsMap[id]!.insert(.editScheduledTime) + } } if let peer = getPeer(id.peerId), let channel = peer as? TelegramChannel { if !message.flags.contains(.Incoming) { @@ -2296,24 +2334,9 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer case .creator, .admin: optionsMap[id]!.insert(.deleteGlobally) case .member: - var hasMediaToReport = false - for media in message.media { - if let _ = media as? TelegramMediaImage { - hasMediaToReport = true - } else if let _ = media as? TelegramMediaFile { - hasMediaToReport = true - } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let _ = content.image { - hasMediaToReport = true - } else if let _ = content.file { - hasMediaToReport = true - } - } - } - if hasMediaToReport { - optionsMap[id]!.insert(.report) - } + break } + optionsMap[id]!.insert(.report) } } else if let user = peer as? TelegramUser { if !isScheduled && message.id.peerId.namespace != Namespaces.Peer.SecretChat && !message.containsSecretMedia && !isAction && !message.id.peerId.isReplies && !message.isCopyProtected() && !isShareProtected { @@ -2634,13 +2657,15 @@ final class ChatReadReportContextItem: ContextMenuCustomItem { fileprivate let context: AccountContext fileprivate let message: Message fileprivate let hasReadReports: Bool + fileprivate let isEdit: Bool fileprivate let stats: MessageReadStats? - fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void + fileprivate let action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void)? - init(context: AccountContext, message: Message, hasReadReports: Bool, stats: MessageReadStats?, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void) { + init(context: AccountContext, message: Message, hasReadReports: Bool, isEdit: Bool, stats: MessageReadStats?, action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void, MessageReadStats?, [StickerPackCollectionInfo], TelegramMediaFile?) -> Void)?) { self.context = context self.message = message self.hasReadReports = hasReadReports + self.isEdit = isEdit self.stats = stats self.action = action } @@ -2719,7 +2744,9 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording self.iconNode = ASImageNode() - if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { + if self.item.isEdit { + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuEditIcon"), color: presentationData.theme.actionSheet.primaryTextColor) + } else if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/MenuReadIcon"), color: presentationData.theme.actionSheet.primaryTextColor) } else if let reactionsAttribute = item.message.reactionsAttribute, !reactionsAttribute.reactions.isEmpty { self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: presentationData.theme.actionSheet.primaryTextColor) @@ -2818,12 +2845,12 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus if let currentStats = self.currentStats { if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { - self.buttonNode.isUserInteractionEnabled = currentStats.peers.isEmpty + self.buttonNode.isUserInteractionEnabled = item.action != nil && currentStats.peers.isEmpty } else { - self.buttonNode.isUserInteractionEnabled = !currentStats.peers.isEmpty || reactionCount != 0 + self.buttonNode.isUserInteractionEnabled = item.action != nil && (!currentStats.peers.isEmpty || reactionCount != 0) } } else { - self.buttonNode.isUserInteractionEnabled = reactionCount != 0 + self.buttonNode.isUserInteractionEnabled = item.action != nil && reactionCount != 0 self.disposable = (item.context.engine.messages.messageReadStats(id: item.message.id) |> deliverOnMainQueue).startStrict(next: { [weak self] value in @@ -2836,7 +2863,9 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus }) } - item.context.account.viewTracker.updateReactionsForMessageIds(messageIds: [item.message.id], force: true) + if !self.item.isEdit { + item.context.account.viewTracker.updateReactionsForMessageIds(messageIds: [item.message.id], force: true) + } } deinit { @@ -2862,9 +2891,9 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus func updateStats(stats: MessageReadStats, transition: ContainedViewLayoutTransition) { if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { - self.buttonNode.isUserInteractionEnabled = stats.peers.isEmpty + self.buttonNode.isUserInteractionEnabled = self.item.action != nil && stats.peers.isEmpty } else { - self.buttonNode.isUserInteractionEnabled = !stats.peers.isEmpty || stats.reactionCount != 0 + self.buttonNode.isUserInteractionEnabled = self.item.action != nil && (!stats.peers.isEmpty || stats.reactionCount != 0) } guard let (calculatedWidth, size) = self.validLayout else { @@ -2908,7 +2937,24 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus reactionCount = currentStats.reactionCount if currentStats.peers.isEmpty { - if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { + if self.item.isEdit, let attribute = self.item.message.attributes.first(where: { $0 is EditedMessageAttribute }) as? EditedMessageAttribute, !attribute.isHidden, attribute.date != 0 { + let dateText = humanReadableStringForTimestamp(strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, timestamp: attribute.date, alwaysShowTime: true, allowYesterday: true, format: HumanReadableStringFormat( + dateFormatString: { value in + return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PrivateMessageEditTimestamp_Date(value).string, ranges: []) + }, + tomorrowFormatString: { value in + return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PrivateMessageEditTimestamp_TodayAt(value).string, ranges: []) + }, + todayFormatString: { value in + return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PrivateMessageEditTimestamp_TodayAt(value).string, ranges: []) + }, + yesterdayFormatString: { value in + return PresentationStrings.FormattedString(string: self.presentationData.strings.Chat_PrivateMessageEditTimestamp_YesterdayAt(value).string, ranges: []) + } + )).string + + self.textNode.attributedText = NSAttributedString(string: dateText, font: Font.regular(floor(self.presentationData.listsFontSize.baseDisplaySize * 0.8)), textColor: self.presentationData.theme.contextMenu.primaryColor) + } else if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { let text = NSAttributedString(string: self.presentationData.strings.Chat_ContextMenuReadDate_ReadAvailablePrefix, font: Font.regular(floor(self.presentationData.listsFontSize.baseDisplaySize * 0.8)), textColor: self.presentationData.theme.contextMenu.primaryColor) if self.textNode.attributedText != text { animatePositions = false @@ -3026,7 +3072,12 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus let positionTransition: ContainedViewLayoutTransition = animatePositions ? transition : .immediate let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0) - let textFrame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + 4.0, y: verticalOrigin), size: textSize) + var textFrame = CGRect(origin: CGPoint(x: sideInset + iconSize.width + 4.0, y: verticalOrigin), size: textSize) + + if self.item.isEdit { + textFrame.origin.x -= 2.0 + } + positionTransition.updateFrameAdditive(node: self.textNode, frame: textFrame) transition.updateAlpha(node: self.textNode, alpha: self.currentStats == nil ? 0.0 : 1.0) @@ -3070,14 +3121,18 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus transition.updateAlpha(node: self.shimmerNode, alpha: self.currentStats == nil ? 1.0 : 0.0) if !iconSize.width.isZero { - transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: sideInset + 1.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) + var iconFrame = CGRect(origin: CGPoint(x: sideInset + 1.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + if self.item.isEdit { + iconFrame.origin.x -= 2.0 + } + transition.updateFrameAdditive(node: self.iconNode, frame: iconFrame) } let avatarsContent: AnimatedAvatarSetContext.Content let placeholderAvatarsContent: AnimatedAvatarSetContext.Content var avatarsPeers: [EnginePeer] = [] - if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser { + if self.item.message.id.peerId.namespace == Namespaces.Peer.CloudUser || self.item.isEdit { } else if let recentPeers = self.item.message.reactionsAttribute?.recentPeers, !recentPeers.isEmpty { for recentPeer in recentPeers { if let peer = self.item.message.peers[recentPeer.peerId] { @@ -3155,12 +3210,15 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus guard let controller = self.getController() else { return } - self.item.action(controller, { [weak self] result in + self.item.action?(controller, { [weak self] result in self?.actionSelected(result) }, self.currentStats, self.customEmojiPacks, self.firstCustomEmojiReaction) } var isActionEnabled: Bool { + if self.item.action == nil { + return false + } var reactionCount = 0 for reaction in mergedMessageReactionsAndPeers(accountPeerId: self.item.context.account.peerId, accountPeer: nil, message: self.item.message).reactions { reactionCount += Int(reaction.count) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index f765fa6430..4cb40d1553 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -114,10 +114,10 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee case .hashtag: break default: - signal = .single({ _ in return .hashtags([]) }) + signal = .single({ _ in return .hashtags([], query) }) } } else { - signal = .single({ _ in return .hashtags([]) }) + signal = .single({ _ in return .hashtags([], query) }) } let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.messages.recentlyUsedHashtags() @@ -129,7 +129,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee result.append(hashtag) } } - return { _ in return .hashtags(result) } + return { _ in return .hashtags(result, query) } } |> castError(ChatContextQueryError.self) diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift index 2a91e8555c..cfcdc9843e 100644 --- a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift @@ -326,6 +326,7 @@ final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode { urlContext: .generic, navigationController: chatController.navigationController as? NavigationController, forceExternal: false, + forceUpdate: false, openPeer: { [weak self] peer, navigation in guard let self, let chatController = interfaceInteraction.chatController() else { return diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 172cc9ef83..d69089f0a3 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -498,12 +498,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { var translateToLanguage: String? if let translationState = interfaceState.translationState, translationState.isEnabled { - translateToLanguage = translationState.toLang - if translateToLanguage == "nb" { - translateToLanguage = "no" - } else if translateToLanguage == "pt-br" { - translateToLanguage = "pt" - } + translateToLanguage = normalizeTranslationLanguage(translationState.toLang) } var currentTranslateToLanguageUpdated = false diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index 8a2fbb0757..aa3a571d83 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -19,34 +19,6 @@ import AnimatedTextComponent private let labelFont = Font.regular(15.0) -private func extractAnimatedTextString(string: PresentationStrings.FormattedString, id: String, mapping: [Int: AnimatedTextComponent.Item.Content]) -> [AnimatedTextComponent.Item] { - var textItems: [AnimatedTextComponent.Item] = [] - - var previousIndex = 0 - let nsString = string.string as NSString - for range in string.ranges.sorted(by: { $0.range.lowerBound < $1.range.lowerBound }) { - if range.range.lowerBound > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_before_\(range.index)"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: range.range.lowerBound - previousIndex))))) - } - if let value = mapping[range.index] { - let isUnbreakable: Bool - switch value { - case .text: - isUnbreakable = true - case .number: - isUnbreakable = false - } - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_item_\(range.index)"), isUnbreakable: isUnbreakable, content: value)) - } - previousIndex = range.range.upperBound - } - if nsString.length > previousIndex { - textItems.append(AnimatedTextComponent.Item(id: AnyHashable("\(id)_text_end"), isUnbreakable: true, content: .text(nsString.substring(with: NSRange(location: previousIndex, length: nsString.length - previousIndex))))) - } - - return textItems -} - final class ChatTagSearchInputPanelNode: ChatInputPanelNode { private struct Params: Equatable { var width: CGFloat @@ -100,6 +72,14 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { private var totalMessageCount: Int? private var totalMessageCountDisposable: Disposable? + public var externalSearchResultsCount: Int32? { + didSet { + if let params = self.currentLayout?.params { + let _ = self.update(params: params, transition: .spring(duration: 0.4)) + } + } + } + override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { } @@ -223,7 +203,15 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { var canChangeListMode = false var resultsTextString: [AnimatedTextComponent.Item] = [] - if let results = params.interfaceState.search?.resultsState { + if let externalSearchResultsCount = self.externalSearchResultsCount { + let value = presentationStringsFormattedNumber(externalSearchResultsCount, params.interfaceState.dateTimeFormat.groupingSeparator) + let suffix = params.interfaceState.strings.Chat_BottomSearchPanel_StoryCount(externalSearchResultsCount) + resultsTextString = [AnimatedTextComponent.Item( + id: "stories", + isUnbreakable: true, + content: .text(params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(value, suffix).string) + )] + } else if let results = params.interfaceState.search?.resultsState { let displayTotalCount = results.completed ? results.messageIndices.count : Int(results.totalCount) if let currentId = results.currentId, let index = results.messageIndices.firstIndex(where: { $0.id == currentId }) { canChangeListMode = true @@ -237,7 +225,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { content: .text(params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat(value, suffix).string) )] } else if params.interfaceState.displayHistoryFilterAsList { - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( ".", "." ), id: "total_count", mapping: [ @@ -247,7 +235,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } else { let adjustedIndex = results.messageIndices.count - 1 - index - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Items_NOfM( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Items_NOfM( ".", "." ), id: "position", mapping: [ @@ -263,7 +251,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } else if let count = self.tagMessageCount?.count ?? self.totalMessageCount { canChangeListMode = count != 0 - resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( + resultsTextString = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( ".", "." ), id: "total_count", mapping: [ @@ -282,7 +270,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } var modeButtonTitle: [AnimatedTextComponent.Item] = [] - modeButtonTitle = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeFormat("."), id: "mode", mapping: [ + modeButtonTitle = AnimatedTextComponent.extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeFormat("."), id: "mode", mapping: [ 0: params.interfaceState.displayHistoryFilterAsList ? .text(params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeChat) : .text(params.interfaceState.strings.Chat_BottomSearchPanel_DisplayModeList) ]) @@ -346,7 +334,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { var nextLeftX: CGFloat = 16.0 - if !self.alwaysShowTotalMessagesCount { + if !self.alwaysShowTotalMessagesCount && self.externalSearchResultsCount == nil { nextLeftX = 12.0 let calendarButtonSize = self.calendarButton.update( transition: .immediate, @@ -372,12 +360,28 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { if let calendarButtonView = self.calendarButton.view { if calendarButtonView.superview == nil { self.view.addSubview(calendarButtonView) + + if !transition.animation.isImmediate { + calendarButtonView.alpha = 1.0 + transition.animateAlpha(view: calendarButtonView, from: 0.0, to: 1.0) + transition.animateScale(view: calendarButtonView, from: 0.01, to: 1.0) + } } transition.setFrame(view: calendarButtonView, frame: calendarButtonFrame) } nextLeftX += calendarButtonSize.width + 8.0 } else if let calendarButtonView = self.calendarButton.view { - calendarButtonView.removeFromSuperview() + if transition.animation.isImmediate { + calendarButtonView.removeFromSuperview() + } else { + transition.setAlpha(view: calendarButtonView, alpha: 0.0, completion: { finished in + if finished { + calendarButtonView.removeFromSuperview() + } + calendarButtonView.alpha = 1.0 + }) + transition.animateScale(view: calendarButtonView, from: 1.0, to: 0.01) + } } if displaySearchMembers { diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 08a9f1aab0..4895dcf260 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1530,7 +1530,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch isEditingMedia = !value.isEmpty isMediaEnabled = !value.isEmpty } else { - isMediaEnabled = false + isMediaEnabled = true } } diff --git a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift index 32ca974cf6..fb7481b9f0 100644 --- a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift @@ -245,12 +245,7 @@ final class ChatTranslationPanelNode: ASDisplayNode { var addedLanguages = Set() var topLanguages: [String] = [] - var langCode = languageCode - if langCode == "nb" { - langCode = "no" - } else if langCode == "pt-br" { - langCode = "pt" - } + let langCode = normalizeTranslationLanguage(languageCode) var selectedLanguages: Set if let ignoredLanguages = settings.ignoredLanguages { diff --git a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift index e755628005..943a4ab4c4 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift @@ -15,34 +15,48 @@ import ChatControllerInteraction import ChatContextQuery import ChatInputContextPanelNode -private struct HashtagChatInputContextPanelEntryStableId: Hashable { - let text: String +private enum HashtagChatInputContextPanelEntryStableId: Hashable { + case generic + case peer + case hashtag(String) } private struct HashtagChatInputContextPanelEntry: Comparable, Identifiable { let index: Int let theme: PresentationTheme - let text: String + let peer: EnginePeer? + let title: String + let text: String? + let badge: String? + let hashtag: String let revealed: Bool + let isAdditionalRecent: Bool var stableId: HashtagChatInputContextPanelEntryStableId { - return HashtagChatInputContextPanelEntryStableId(text: self.text) + switch self.index { + case 0: + return .generic + case 1: + return .peer + default: + return .hashtag(self.title) + } } func withUpdatedTheme(_ theme: PresentationTheme) -> HashtagChatInputContextPanelEntry { - return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, text: self.text, revealed: self.revealed) + return HashtagChatInputContextPanelEntry(index: self.index, theme: theme, peer: peer, title: self.title, text: self.text, badge: self.badge, hashtag: self.hashtag, revealed: self.revealed, isAdditionalRecent: self.isAdditionalRecent) } static func ==(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { - return lhs.index == rhs.index && lhs.text == rhs.text && lhs.theme === rhs.theme && lhs.revealed == rhs.revealed + return lhs.index == rhs.index && lhs.peer == rhs.peer && lhs.title == rhs.title && lhs.text == rhs.text && lhs.badge == rhs.badge && lhs.hashtag == rhs.hashtag && lhs.theme === rhs.theme && lhs.revealed == rhs.revealed && lhs.isAdditionalRecent == rhs.isAdditionalRecent } static func <(lhs: HashtagChatInputContextPanelEntry, rhs: HashtagChatInputContextPanelEntry) -> Bool { return lhs.index < rhs.index } - func item(account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem { - return HashtagChatInputPanelItem(presentationData: ItemListPresentationData(presentationData), text: self.text, revealed: self.revealed, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested) + func item(context: AccountContext, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> ListViewItem { + return HashtagChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), peer: self.peer, title: self.title, text: self.text, badge: self.badge, hashtag: self.hashtag, revealed: self.revealed, isAdditionalRecent: self.isAdditionalRecent, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested) } } @@ -52,12 +66,12 @@ private struct HashtagChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], account: Account, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [HashtagChatInputContextPanelEntry], to toEntries: [HashtagChatInputContextPanelEntry], context: AccountContext, presentationData: PresentationData, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) -> HashtagChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, setHashtagRevealed: setHashtagRevealed, hashtagSelected: hashtagSelected, removeRequested: removeRequested), directionHint: nil) } return HashtagChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -67,6 +81,8 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private var currentEntries: [HashtagChatInputContextPanelEntry]? private var currentResults: [String] = [] + private var currentQuery: String = "" + private var currentPeer: EnginePeer? private var revealedHashtag: String? private var enqueuedTransitions: [(HashtagChatInputContextPanelTransition, Bool)] = [] @@ -91,14 +107,76 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { self.addSubnode(self.listView) } - func updateResults(_ results: [String]) { + func updateResults(_ results: [String], query: String, peer: EnginePeer?) { self.currentResults = results + self.currentQuery = query + self.currentPeer = peer var entries: [HashtagChatInputContextPanelEntry] = [] var index = 0 var stableIds = Set() - for text in results { - let entry = HashtagChatInputContextPanelEntry(index: index, theme: self.theme, text: text, revealed: text == self.revealedHashtag) + + var isAdditionalRecent = false + if let peer, let _ = peer.addressName { + isAdditionalRecent = true + } + //TODO:localize + if query.count > 3 { + if let peer, let addressName = peer.addressName { + let genericEntry = HashtagChatInputContextPanelEntry( + index: 0, + theme: self.theme, + peer: nil, + title: "Use #\(query)", + text: "searches posts from all channels", + badge: nil, + hashtag: query, + revealed: false, + isAdditionalRecent: false + ) + stableIds.insert(genericEntry.stableId) + entries.append(genericEntry) + + var isGroup = false + if case let .channel(channel) = peer, case .group = channel.info { + isGroup = true + } + let peerEntry = HashtagChatInputContextPanelEntry( + index: 1, + theme: self.theme, + peer: peer, + title: "Use #\(query)@\(addressName)", + text: isGroup ? "searches only posts from this group" : "searches only posts from this channel", + badge: "NEW", + hashtag: "\(query)@\(addressName)", + revealed: false, + isAdditionalRecent: false + ) + stableIds.insert(peerEntry.stableId) + entries.append(peerEntry) + } + } + + index = 2 + + for hashtag in results { + if hashtag == query { + continue + } + if !hashtag.hasPrefix(query) { + continue + } + let entry = HashtagChatInputContextPanelEntry( + index: index, + theme: self.theme, + peer: hashtag.contains("@") ? peer : nil, + title: "#\(hashtag)", + text: nil, + badge: nil, + hashtag: hashtag, + revealed: hashtag == self.revealedHashtag, + isAdditionalRecent: isAdditionalRecent && !hashtag.contains("@") + ) if stableIds.contains(entry.stableId) { continue } @@ -112,10 +190,10 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { private func prepareTransition(from: [HashtagChatInputContextPanelEntry]? , to: [HashtagChatInputContextPanelEntry]) { let firstTime = from == nil let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, presentationData: presentationData, setHashtagRevealed: { [weak self] text in + let transition = preparedTransition(from: from ?? [], to: to, context: self.context, presentationData: presentationData, setHashtagRevealed: { [weak self] text in if let strongSelf = self { strongSelf.revealedHashtag = text - strongSelf.updateResults(strongSelf.currentResults) + strongSelf.updateResults(strongSelf.currentResults, query: strongSelf.currentQuery, peer: strongSelf.currentPeer) } }, hashtagSelected: { [weak self] text in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { @@ -131,8 +209,7 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { if let range = hashtagQueryRange { let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) - let replacementText = text + " " - + let replacementText = text inputText.replaceCharacters(in: range, with: replacementText) let selectionPosition = range.lowerBound + (replacementText as NSString).length @@ -172,7 +249,11 @@ final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode { //options.insert(.LowLatency) } else { options.insert(.AnimateTopItemPosition) - options.insert(.AnimateCrossfade) + if transition.insertions.isEmpty && transition.deletions.isEmpty && transition.updates.count <= 2 { + options.insert(.AnimateInsertion) + } else { + options.insert(.AnimateCrossfade) + } } var insets = UIEdgeInsets() diff --git a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift index dfccdec0f2..899cbe278a 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift @@ -8,21 +8,35 @@ import Postbox import TelegramPresentationData import TelegramUIPreferences import ItemListUI +import AvatarNode +import AccountContext final class HashtagChatInputPanelItem: ListViewItem { + fileprivate let context: AccountContext fileprivate let presentationData: ItemListPresentationData - fileprivate let text: String + fileprivate let peer: EnginePeer? + fileprivate let title: String + fileprivate let text: String? + fileprivate let badge: String? + fileprivate let hashtag: String fileprivate let revealed: Bool + fileprivate let isAdditionalRecent: Bool fileprivate let setHashtagRevealed: (String?) -> Void private let hashtagSelected: (String) -> Void fileprivate let removeRequested: (String) -> Void let selectable: Bool = true - public init(presentationData: ItemListPresentationData, text: String, revealed: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) { + public init(context: AccountContext, presentationData: ItemListPresentationData, peer: EnginePeer?, title: String, text: String?, badge: String? = nil, hashtag: String, revealed: Bool, isAdditionalRecent: Bool, setHashtagRevealed: @escaping (String?) -> Void, hashtagSelected: @escaping (String) -> Void, removeRequested: @escaping (String) -> Void) { + self.context = context self.presentationData = presentationData + self.peer = peer + self.title = title self.text = text + self.badge = badge + self.hashtag = hashtag self.revealed = revealed + self.isAdditionalRecent = isAdditionalRecent self.setHashtagRevealed = setHashtagRevealed self.hashtagSelected = hashtagSelected self.removeRequested = removeRequested @@ -79,14 +93,29 @@ final class HashtagChatInputPanelItem: ListViewItem { if self.revealed { self.setHashtagRevealed(nil) } else { - self.hashtagSelected(self.text) +// if self.isAdditionalRecent { +// self.hashtagSelected(self.hashtag) +// } else { + self.hashtagSelected(self.hashtag + " ") +// } } } } +private let avatarFont = avatarPlaceholderFont(size: 16.0) + final class HashtagChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 42.0 + + private let iconBackgroundLayer = SimpleLayer() + private let iconLayer = SimpleLayer() + private var avatarNode: AvatarNode? + + private let badgeBackgroundLayer = SimpleLayer() + + private let titleNode: TextNode private let textNode: TextNode + private let badgeNode: TextNode private let topSeparatorNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -105,7 +134,12 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { private var validLayout: (CGSize, CGFloat, CGFloat)? init() { + self.iconBackgroundLayer.cornerRadius = 15.0 + self.badgeBackgroundLayer.cornerRadius = 4.0 + + self.titleNode = TextNode() self.textNode = TextNode() + self.badgeNode = TextNode() self.topSeparatorNode = ASDisplayNode() self.topSeparatorNode.isLayerBacked = true @@ -120,9 +154,10 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { self.activateAreaNode.accessibilityTraits = [.button] super.init(layerBacked: false, dynamicBounce: false) - + self.addSubnode(self.topSeparatorNode) self.addSubnode(self.separatorNode) + self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.activateAreaNode) @@ -131,6 +166,12 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { override func didLoad() { super.didLoad() + self.view.layer.addSublayer(self.iconBackgroundLayer) + self.iconBackgroundLayer.addSublayer(self.iconLayer) + + self.view.layer.addSublayer(self.badgeBackgroundLayer) + self.addSubnode(self.badgeNode) + let recognizer = ItemListRevealOptionsGestureRecognizer(target: self, action: #selector(self.revealGesture(_:))) self.recognizer = recognizer recognizer.allowAnyDirection = false @@ -149,16 +190,24 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { } func asyncLayout() -> (_ item: HashtagChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) + let makeBadgeLayout = TextNode.asyncLayout(self.badgeNode) + return { [weak self] item, params, mergedTop, mergedBottom in - let textFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) - - let baseWidth = params.width - params.leftInset - params.rightInset + let titleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)) + let badgeFont = Font.medium(floor(item.presentationData.fontSize.baseDisplaySize * 10.0 / 17.0)) let leftInset: CGFloat = 15.0 + params.leftInset + let textLeftInset: CGFloat = 40.0 + let baseWidth = params.width - params.leftInset - params.rightInset - textLeftInset - let title = "#\(item.text)" - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (badgeLayout, badgeApply) = makeBadgeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.badge ?? "", font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth - badgeLayout.size.width, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text ?? "", font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) @@ -166,26 +215,70 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.validLayout = (nodeLayout.contentSize, params.leftInset, params.rightInset) - + let revealOffset = strongSelf.revealOffset + if strongSelf.iconLayer.contents == nil { + strongSelf.iconLayer.contents = UIImage(bundleImageName: "Chat/Hashtag/SuggestHashtag")?.cgImage + } + strongSelf.iconBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemAccentColor.cgColor + strongSelf.iconLayer.layerTintColor = item.presentationData.theme.list.itemCheckColors.foregroundColor.cgColor + strongSelf.badgeBackgroundLayer.backgroundColor = item.presentationData.theme.list.itemAccentColor.cgColor + strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor strongSelf.topSeparatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor strongSelf.backgroundColor = item.presentationData.theme.list.plainBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + let _ = titleApply() let _ = textApply() - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: floor((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) + let _ = badgeApply() + + if textLayout.size.height > 0.0 { + let combinedHeight = titleLayout.size.height + textLayout.size.height + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - combinedHeight) / 2.0)), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - combinedHeight) / 2.0) + combinedHeight - textLayout.size.height), size: textLayout.size) + } else { + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: revealOffset + leftInset + textLeftInset, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + } + + if badgeLayout.size.height > 0.0 { + let badgeFrame = CGRect(origin: CGPoint(x: strongSelf.titleNode.frame.maxX + 8.0, y: floorToScreenPixels(strongSelf.titleNode.frame.midY - badgeLayout.size.height / 2.0)), size: badgeLayout.size) + let badgeBackgroundFrame = badgeFrame.insetBy(dx: -3.0, dy: -2.0) + + strongSelf.badgeNode.frame = badgeFrame + strongSelf.badgeBackgroundLayer.frame = badgeBackgroundFrame + } strongSelf.topSeparatorNode.isHidden = mergedTop strongSelf.separatorNode.isHidden = !mergedBottom + let iconSize = CGSize(width: 30.0, height: 30.0) + strongSelf.iconBackgroundLayer.frame = CGRect(origin: CGPoint(x: params.leftInset + 12.0, y: floor((nodeLayout.contentSize.height - 30.0) / 2.0)), size: iconSize) + strongSelf.iconLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 30.0, height: 30.0)) + + if let peer = item.peer { + strongSelf.iconBackgroundLayer.isHidden = true + let avatarNode: AvatarNode + if let current = strongSelf.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarFont) + strongSelf.addSubnode(avatarNode) + strongSelf.avatarNode = avatarNode + } + avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer) + avatarNode.frame = strongSelf.iconBackgroundLayer.frame + } else { + strongSelf.iconBackgroundLayer.isHidden = false + } + strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset + textLeftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - textLeftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) - strongSelf.activateAreaNode.accessibilityLabel = title + strongSelf.activateAreaNode.accessibilityLabel = item.title strongSelf.activateAreaNode.frame = CGRect(origin: .zero, size: nodeLayout.size) strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]) @@ -197,7 +290,8 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { if let (_, leftInset, _) = self.validLayout { - transition.updateFrameAdditive(node: self.textNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 15.0 + leftInset, y: self.textNode.frame.minY), size: self.textNode.frame.size)) + transition.updateFrameAdditive(layer: self.iconBackgroundLayer, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 12.0 + leftInset, y: self.iconBackgroundLayer.frame.minY), size: self.iconBackgroundLayer.frame.size)) + transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: min(offset, 0.0) + 15.0 + 40.0 + leftInset, y: self.titleNode.frame.minY), size: self.titleNode.frame.size)) } } @@ -276,6 +370,11 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let item = self.item { + if let _ = item.text { + return false + } + } return true } @@ -356,7 +455,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { guard let item = self.item else { return } - item.removeRequested(item.text) + item.removeRequested(item.hashtag) } private func setupAndAddRevealNode() { diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index a7712b527b..889f521c1d 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -250,9 +250,9 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { let subject: BrowserScreen.Subject if file.mimeType == "application/pdf" { - subject = .pdfDocument(file: file, canShare: canShare) + subject = .pdfDocument(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) } else { - subject = .document(file: file, canShare: canShare) + subject = .document(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) } let controller = BrowserScreen(context: params.context, subject: subject) controller.openDocument = { file, canShare in diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 980473852c..acb60a6399 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -58,6 +58,7 @@ func openResolvedUrlImpl( urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, + forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, @@ -222,7 +223,16 @@ func openResolvedUrlImpl( case let .stickerPack(name, _): dismissInput() - let controller = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: .name(name), stickerPacks: [.name(name)], parentNavigationController: navigationController, sendSticker: sendSticker, sendEmoji: sendEmoji, actionPerformed: { actions in + let controller = StickerPackScreen( + context: context, + updatedPresentationData: updatedPresentationData, + mainStickerPack: .name(name), + stickerPacks: [.name(name)], + ignoreCache: forceUpdate, + parentNavigationController: navigationController, + sendSticker: sendSticker, + sendEmoji: sendEmoji, + actionPerformed: { actions in if actions.count > 1, let first = actions.first { if case .add = first.2 { present(UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.EmojiPackActionInfo_AddedTitle, text: presentationData.strings.EmojiPackActionInfo_MultipleAddedText(Int32(actions.count)), undo: false, info: first.0, topItem: first.1.first, context: context), elevatedLayout: true, animateInAsReplacement: false, action: { _ in diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 919032eb4d..69149b7426 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -197,7 +197,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if case let .externalUrl(value) = resolved { context.sharedContext.applicationBindings.openUrl(value) } else { - context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in switch navigation { case .info: if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 94e55db6ee..f51c17d359 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -166,6 +166,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 8213dafa0c..30a0afd453 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1614,8 +1614,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) } - public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) { - openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, forceExternal: forceExternal, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, sendEmoji: sendEmoji, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext, progress: progress, completion: completion) + public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, forceUpdate: Bool, openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) { + openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, forceExternal: forceExternal, forceUpdate: forceUpdate, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, sendEmoji: sendEmoji, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext, progress: progress, completion: completion) } public func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController { @@ -1781,6 +1781,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _, _, _, _ in + }, adContextAction: { _, _, _ in + }, removeAd: { _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { @@ -1924,8 +1926,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return inputPanelNode } - public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController { - return HashtagSearchController(context: context, peer: peer, query: query, all: all) + public func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController { + return HashtagSearchController(context: context, peer: peer, query: query, mode: stories ? .chatOnly : .generic, stories: stories, forceDark: forceDark) } public func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController { @@ -2337,10 +2339,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { let giftController = GiftOptionsScreen(context: context, starsContext: starsContext, peerId: peer.id, premiumOptions: premiumOptions) giftController.navigationPresentation = .modal controller?.push(giftController) - - if case .chatList = source, let _ = currentBirthdays { - let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() - } } }) @@ -2366,7 +2364,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, - avatarInitiallyExpanded: true, + avatarInitiallyExpanded: peer.smallProfileImage != nil, fromChat: false, requestsContext: nil ) { diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 5aae48a9d0..ee253628e0 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -26,7 +26,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n guard let peer = peer else { return } - context.sharedContext.openResolvedUrl(.peer(peer._asPeer(), navigation), context: context, urlContext: .generic, navigationController: (controller?.navigationController as? NavigationController), forceExternal: false, openPeer: { (peer, navigation) in + context.sharedContext.openResolvedUrl(.peer(peer._asPeer(), navigation), context: context, urlContext: .generic, navigationController: (controller?.navigationController as? NavigationController), forceExternal: false, forceUpdate: false, openPeer: { (peer, navigation) in switch navigation { case let .chat(_, subject, peekData): if let navigationController = controller?.navigationController as? NavigationController { @@ -102,7 +102,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n (controller.navigationController as? NavigationController)?.pushViewController(browserController, animated: true) case .boost, .chatFolder, .join: if let navigationController = controller.navigationController as? NavigationController { - openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, message: nil, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigateToPeer in + openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, message: nil, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigateToPeer in openResolvedPeerImpl(peer, navigateToPeer) }, sendFile: nil, sendSticker: nil, sendEmoji: nil, joinVoiceChat: nil, present: { c, a in controller.present(c, in: .window(.root), with: a) diff --git a/submodules/TelegramUniversalVideoContent/BUILD b/submodules/TelegramUniversalVideoContent/BUILD index b705ae876b..2818802355 100644 --- a/submodules/TelegramUniversalVideoContent/BUILD +++ b/submodules/TelegramUniversalVideoContent/BUILD @@ -1,4 +1,44 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "HlsBundleContents", + srcs = glob([ + "HlsBundle/**", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "HlsBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.TelegramUniversalVideoContent + CFBundleDevelopmentRegion + en + CFBundleName + TelegramUniversalVideoContent + """ +) + +apple_resource_bundle( + name = "HlsBundle", + infoplists = [ + ":HlsBundleInfoPlist", + ], + resources = [ + ":HlsBundleContents", + ], +) swift_library( name = "TelegramUniversalVideoContent", @@ -9,6 +49,9 @@ swift_library( copts = [ "-warnings-as-errors", ], + data = [ + ":HlsBundle", + ], deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/TelegramUniversalVideoContent/HlsBundle/index.bundle.js b/submodules/TelegramUniversalVideoContent/HlsBundle/index.bundle.js new file mode 100644 index 0000000000..961d4bc696 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/HlsBundle/index.bundle.js @@ -0,0 +1 @@ +(()=>{"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,s,i,r,n,a={exports:{}};e=/^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/,s=/^(?=([^\/?#]*))\1([^]*)$/,i=/(?:\/|^)\.(?=\/)/g,r=/(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g,a.exports=n={buildAbsoluteURL:function(t,e,i){if(i=i||{},t=t.trim(),!(e=e.trim())){if(!i.alwaysNormalize)return t;var r=n.parseURL(t);if(!r)throw new Error("Error trying to parse base URL.");return r.path=n.normalizePath(r.path),n.buildURLFromParts(r)}var a=n.parseURL(e);if(!a)throw new Error("Error trying to parse relative URL.");if(a.scheme)return i.alwaysNormalize?(a.path=n.normalizePath(a.path),n.buildURLFromParts(a)):e;var o=n.parseURL(t);if(!o)throw new Error("Error trying to parse base URL.");if(!o.netLoc&&o.path&&"/"!==o.path[0]){var l=s.exec(o.path);o.netLoc=l[1],o.path=l[2]}o.netLoc&&!o.path&&(o.path="/");var h={scheme:o.scheme,netLoc:a.netLoc,path:null,params:a.params,query:a.query,fragment:a.fragment};if(!a.netLoc&&(h.netLoc=o.netLoc,"/"!==a.path[0]))if(a.path){var d=o.path,c=d.substring(0,d.lastIndexOf("/")+1)+a.path;h.path=n.normalizePath(c)}else h.path=o.path,a.params||(h.params=o.params,a.query||(h.query=o.query));return null===h.path&&(h.path=i.alwaysNormalize?n.normalizePath(a.path):a.path),n.buildURLFromParts(h)},parseURL:function(t){var s=e.exec(t);return s?{scheme:s[1]||"",netLoc:s[2]||"",path:s[3]||"",params:s[4]||"",query:s[5]||"",fragment:s[6]||""}:null},normalizePath:function(t){for(t=t.split("").reverse().join("").replace(i,"");t.length!==(t=t.replace(r,"")).length;);return t.split("").reverse().join("")},buildURLFromParts:function(t){return t.scheme+t.netLoc+t.path+t.params+t.query+t.fragment}};var o=a.exports;function l(t,e){var s=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),s.push.apply(s,i)}return s}function h(t){for(var e=1;e`):E}(e)}))}const A=S,R=/^(\d+)x(\d+)$/,b=/(.+?)=(".*?"|.*?)(?:,|$)/g;class k{constructor(t){"string"==typeof t&&(t=k.parseAttrList(t)),u(this,t)}get clientAttrs(){return Object.keys(this).filter((t=>"X-"===t.substring(0,2)))}decimalInteger(t){const e=parseInt(this[t],10);return e>Number.MAX_SAFE_INTEGER?1/0:e}hexadecimalInteger(t){if(this[t]){let e=(this[t]||"0x").slice(2);e=(1&e.length?"0":"")+e;const s=new Uint8Array(e.length/2);for(let t=0;tNumber.MAX_SAFE_INTEGER?1/0:e}decimalFloatingPoint(t){return parseFloat(this[t])}optionalFloat(t,e){const s=this[t];return s?parseFloat(s):e}enumeratedString(t){return this[t]}bool(t){return"YES"===this[t]}decimalResolution(t){const e=R.exec(this[t]);if(null!==e)return{width:parseInt(e[1],10),height:parseInt(e[2],10)}}static parseAttrList(t){let e;const s={};for(b.lastIndex=0;null!==(e=b.exec(t));){let t=e[2];0===t.indexOf('"')&&t.lastIndexOf('"')===t.length-1&&(t=t.slice(1,-1));s[e[1].trim()]=t}return s}}function w(t){return"SCTE35-OUT"===t||"SCTE35-IN"===t}class D{constructor(t,e){if(this.attr=void 0,this._startDate=void 0,this._endDate=void 0,this._badValueForSameId=void 0,e){const s=e.attr;for(const e in s)if(Object.prototype.hasOwnProperty.call(t,e)&&t[e]!==s[e]){A.warn(`DATERANGE tag attribute: "${e}" does not match for tags with ID: "${t.ID}"`),this._badValueForSameId=e;break}t=u(new k({}),s,t)}if(this.attr=t,this._startDate=new Date(t["START-DATE"]),"END-DATE"in this.attr){const t=new Date(this.attr["END-DATE"]);f(t.getTime())&&(this._endDate=t)}}get id(){return this.attr.ID}get class(){return this.attr.CLASS}get startDate(){return this._startDate}get endDate(){if(this._endDate)return this._endDate;const t=this.duration;return null!==t?new Date(this._startDate.getTime()+1e3*t):null}get duration(){if("DURATION"in this.attr){const t=this.attr.decimalFloatingPoint("DURATION");if(f(t))return t}else if(this._endDate)return(this._endDate.getTime()-this._startDate.getTime())/1e3;return null}get plannedDuration(){return"PLANNED-DURATION"in this.attr?this.attr.decimalFloatingPoint("PLANNED-DURATION"):null}get endOnNext(){return this.attr.bool("END-ON-NEXT")}get isValid(){return!!this.id&&!this._badValueForSameId&&f(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)}}class I{constructor(){this.aborted=!1,this.loaded=0,this.retry=0,this.total=0,this.chunkCount=0,this.bwEstimate=0,this.loading={start:0,first:0,end:0},this.parsing={start:0,end:0},this.buffering={start:0,first:0,end:0}}}var _="audio",C="video",x="audiovideo";class P{constructor(t){this._byteRange=null,this._url=null,this.baseurl=void 0,this.relurl=void 0,this.elementaryStreams={[_]:null,[C]:null,[x]:null},this.baseurl=t}setByteRange(t,e){const s=t.split("@",2);let i;i=1===s.length?(null==e?void 0:e.byteRangeEndOffset)||0:parseInt(s[1]),this._byteRange=[i,parseInt(s[0])+i]}get byteRange(){return this._byteRange?this._byteRange:[]}get byteRangeStartOffset(){return this.byteRange[0]}get byteRangeEndOffset(){return this.byteRange[1]}get url(){return!this._url&&this.baseurl&&this.relurl&&(this._url=o.buildAbsoluteURL(this.baseurl,this.relurl,{alwaysNormalize:!0})),this._url||""}set url(t){this._url=t}}class M extends P{constructor(t,e){super(e),this._decryptdata=null,this.rawProgramDateTime=null,this.programDateTime=null,this.tagList=[],this.duration=0,this.sn=0,this.levelkeys=void 0,this.type=void 0,this.loader=null,this.keyLoader=null,this.level=-1,this.cc=0,this.startPTS=void 0,this.endPTS=void 0,this.startDTS=void 0,this.endDTS=void 0,this.start=0,this.deltaPTS=void 0,this.maxStartPTS=void 0,this.minEndPTS=void 0,this.stats=new I,this.data=void 0,this.bitrateTest=!1,this.title=null,this.initSegment=null,this.endList=void 0,this.gap=void 0,this.urlId=0,this.type=t}get decryptdata(){const{levelkeys:t}=this;if(!t&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkeys&&!this.levelkeys.NONE){const t=this.levelkeys.identity;if(t)this._decryptdata=t.getDecryptData(this.sn);else{const t=Object.keys(this.levelkeys);if(1===t.length)return this._decryptdata=this.levelkeys[t[0]].getDecryptData(this.sn)}}return this._decryptdata}get end(){return this.start+this.duration}get endProgramDateTime(){if(null===this.programDateTime)return null;if(!f(this.programDateTime))return null;const t=f(this.duration)?this.duration:0;return this.programDateTime+1e3*t}get encrypted(){var t;if(null!=(t=this._decryptdata)&&t.encrypted)return!0;if(this.levelkeys){const t=Object.keys(this.levelkeys),e=t.length;if(e>1||1===e&&this.levelkeys[t[0]].encrypted)return!0}return!1}setKeyFormat(t){if(this.levelkeys){const e=this.levelkeys[t];e&&!this._decryptdata&&(this._decryptdata=e.getDecryptData(this.sn))}}abortRequests(){var t,e;null==(t=this.loader)||t.abort(),null==(e=this.keyLoader)||e.abort()}setElementaryStreamInfo(t,e,s,i,r,n=!1){const{elementaryStreams:a}=this,o=a[t];o?(o.startPTS=Math.min(o.startPTS,e),o.endPTS=Math.max(o.endPTS,s),o.startDTS=Math.min(o.startDTS,i),o.endDTS=Math.max(o.endDTS,r)):a[t]={startPTS:e,endPTS:s,startDTS:i,endDTS:r,partial:n}}clearElementaryStreamInfo(){const{elementaryStreams:t}=this;t[_]=null,t[C]=null,t[x]=null}}class F extends P{constructor(t,e,s,i,r){super(s),this.fragOffset=0,this.duration=0,this.gap=!1,this.independent=!1,this.relurl=void 0,this.fragment=void 0,this.index=void 0,this.stats=new I,this.duration=t.decimalFloatingPoint("DURATION"),this.gap=t.bool("GAP"),this.independent=t.bool("INDEPENDENT"),this.relurl=t.enumeratedString("URI"),this.fragment=e,this.index=i;const n=t.enumeratedString("BYTERANGE");n&&this.setByteRange(n,r),r&&(this.fragOffset=r.fragOffset+r.duration)}get start(){return this.fragment.start+this.fragOffset}get end(){return this.start+this.duration}get loaded(){const{elementaryStreams:t}=this;return!!(t.audio||t.video||t.audiovideo)}}class O{constructor(t){this.PTSKnown=!1,this.alignedSliding=!1,this.averagetargetduration=void 0,this.endCC=0,this.endSN=0,this.fragments=void 0,this.fragmentHint=void 0,this.partList=null,this.dateRanges=void 0,this.live=!0,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.availabilityDelay=void 0,this.misses=0,this.startCC=0,this.startSN=0,this.startTimeOffset=null,this.targetduration=0,this.totalduration=0,this.type=null,this.url=void 0,this.m3u8="",this.version=null,this.canBlockReload=!1,this.canSkipUntil=0,this.canSkipDateRanges=!1,this.skippedSegments=0,this.recentlyRemovedDateranges=void 0,this.partHoldBack=0,this.holdBack=0,this.partTarget=0,this.preloadHint=void 0,this.renditionReports=void 0,this.tuneInGoal=0,this.deltaUpdateFailed=void 0,this.driftStartTime=0,this.driftEndTime=0,this.driftStart=0,this.driftEnd=0,this.encryptedFragments=void 0,this.playlistParsingError=null,this.variableList=null,this.hasVariableRefs=!1,this.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=t}reloaded(t){if(!t)return this.advanced=!0,void(this.updated=!0);const e=this.lastPartSn-t.lastPartSn,s=this.lastPartIndex-t.lastPartIndex;this.updated=this.endSN!==t.endSN||!!s||!!e||!this.live,this.advanced=this.endSN>t.endSN||e>0||0===e&&s>0,this.updated||this.advanced?this.misses=Math.floor(.6*t.misses):this.misses=t.misses+1,this.availabilityDelay=t.availabilityDelay}get hasProgramDateTime(){return!!this.fragments.length&&f(this.fragments[this.fragments.length-1].programDateTime)}get levelTargetDuration(){return this.averagetargetduration||this.targetduration||10}get drift(){const t=this.driftEndTime-this.driftStartTime;if(t>0){return 1e3*(this.driftEnd-this.driftStart)/t}return 1}get edge(){return this.partEnd||this.fragmentEnd}get partEnd(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}get fragmentEnd(){var t;return null!=(t=this.fragments)&&t.length?this.fragments[this.fragments.length-1].end:0}get age(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}get lastPartIndex(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].index:-1}get lastPartSn(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}function N(t){return Uint8Array.from(atob(t),(t=>t.charCodeAt(0)))}function U(t){const e=t.split(":");let s=null;if("data"===e[0]&&2===e.length){const t=e[1].split(";"),i=t[t.length-1].split(",");if(2===i.length){const e="base64"===i[0],r=i[1];e?(t.splice(-1,1),s=N(r)):s=function(t){const e=B(t).subarray(0,16),s=new Uint8Array(16);return s.set(e,16-e.length),s}(r)}}return s}function B(t){return Uint8Array.from(unescape(encodeURIComponent(t)),(t=>t.charCodeAt(0)))}const $="undefined"!=typeof self?self:void 0;var G={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},K="org.w3.clearkey",H="com.apple.streamingkeydelivery",V="com.microsoft.playready",Y="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function W(t){switch(t){case H:return G.FAIRPLAY;case V:return G.PLAYREADY;case Y:return G.WIDEVINE;case K:return G.CLEARKEY}}var j="1077efecc0b24d02ace33c1e52e2fb4b",q="e2719d58a985b3c9781ab030af78d30e",X="9a04f07998404286ab92e65be0885f95",z="edef8ba979d64acea3c827dcd51d21ed";function Q(t){return t===z?G.WIDEVINE:t===X?G.PLAYREADY:t===j||t===q?G.CLEARKEY:void 0}function J(t){switch(t){case G.FAIRPLAY:return H;case G.PLAYREADY:return V;case G.WIDEVINE:return Y;case G.CLEARKEY:return K}}function Z(t){const{drmSystems:e,widevineLicenseUrl:s}=t,i=e?[G.FAIRPLAY,G.WIDEVINE,G.PLAYREADY,G.CLEARKEY].filter((t=>!!e[t])):[];return!i[G.WIDEVINE]&&s&&i.push(G.WIDEVINE),i}const tt=null!=$&&null!=(et=$.navigator)&&et.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;var et;function st(t,e,s){return Uint8Array.prototype.slice?t.slice(e,s):new Uint8Array(Array.prototype.slice.call(t,e,s))}const it=(t,e)=>e+10<=t.length&&73===t[e]&&68===t[e+1]&&51===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128,rt=(t,e)=>e+10<=t.length&&51===t[e]&&68===t[e+1]&&73===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128,nt=(t,e)=>{const s=e;let i=0;for(;it(t,e);){i+=10;i+=at(t,e+6),rt(t,e+10)&&(i+=10),e+=i}if(i>0)return t.subarray(s,s+i)},at=(t,e)=>{let s=0;return s=(127&t[e])<<21,s|=(127&t[e+1])<<14,s|=(127&t[e+2])<<7,s|=127&t[e+3],s},ot=(t,e)=>it(t,e)&&at(t,e+6)+10<=t.length-e,lt=t=>{const e=ct(t);for(let t=0;tt&&"PRIV"===t.key&&"com.apple.streaming.transportStreamTimestamp"===t.info,dt=t=>{const e=String.fromCharCode(t[0],t[1],t[2],t[3]),s=at(t,4);return{type:e,size:s,data:t.subarray(10,10+s)}},ct=t=>{let e=0;const s=[];for(;it(t,e);){const i=at(t,e+6);e+=10;const r=e+i;for(;e+8"PRIV"===t.type?ft(t):"W"===t.type[0]?mt(t):gt(t),ft=t=>{if(t.size<2)return;const e=vt(t.data,!0),s=new Uint8Array(t.data.subarray(e.length+1));return{key:t.type,info:e,data:s.buffer}},gt=t=>{if(t.size<2)return;if("TXXX"===t.type){let e=1;const s=vt(t.data.subarray(e),!0);e+=s.length+1;const i=vt(t.data.subarray(e));return{key:t.type,info:s,data:i}}const e=vt(t.data.subarray(1));return{key:t.type,data:e}},mt=t=>{if("WXXX"===t.type){if(t.size<2)return;let e=1;const s=vt(t.data.subarray(e),!0);e+=s.length+1;const i=vt(t.data.subarray(e));return{key:t.type,info:s,data:i}}const e=vt(t.data);return{key:t.type,data:e}},pt=t=>{if(8===t.data.byteLength){const e=new Uint8Array(t.data),s=1&e[3];let i=(e[4]<<23)+(e[5]<<15)+(e[6]<<7)+e[7];return i/=45,s&&(i+=47721858.84),Math.round(i)}},vt=(t,e=!1)=>{const s=Et();if(s){const i=s.decode(t);if(e){const t=i.indexOf("\0");return-1!==t?i.substring(0,t):i}return i.replace(/\0/g,"")}const i=t.length;let r,n,a,o="",l=0;for(;l>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:o+=String.fromCharCode(r);break;case 12:case 13:n=t[l++],o+=String.fromCharCode((31&r)<<6|63&n);break;case 14:n=t[l++],a=t[l++],o+=String.fromCharCode((15&r)<<12|(63&n)<<6|63&a)}}return o};let yt;function Et(){if(!navigator.userAgent.includes("PlayStation 4"))return yt||void 0===self.TextDecoder||(yt=new self.TextDecoder("utf-8")),yt}const Tt=function(t){let e="";for(let s=0;s>24,t[e+1]=s>>16&255,t[e+2]=s>>8&255,t[e+3]=255&s}function _t(t,e){const s=[];if(!e.length)return s;const i=t.byteLength;for(let r=0;r1?r+n:i;if(Rt(t.subarray(r+4,r+8))===e[0])if(1===e.length)s.push(t.subarray(r+8,a));else{const i=_t(t.subarray(r+8,a),e.slice(1));i.length&&Lt.apply(s,i)}r=a}return s}function Ct(t){const e=[],s=t[0];let i=8;const r=kt(t,i);i+=4;let n=0,a=0;0===s?(n=kt(t,i),a=kt(t,i+4),i+=8):(n=wt(t,i),a=wt(t,i+8),i+=16),i+=2;let o=t.length+a;const l=bt(t,i);i+=2;for(let s=0;s>>31)return A.warn("SIDX has hierarchical references (not supported)"),null;const l=kt(t,s);s+=4,e.push({referenceSize:a,subsegmentDuration:l,info:{duration:l/r,start:o,end:o+a-1}}),o+=a,s+=4,i=s}return{earliestPresentationTime:n,timescale:r,version:s,referencesCount:l,references:e}}function xt(t){const e=[],s=_t(t,["moov","trak"]);for(let t=0;t{const s=kt(t,4),i=e[s];i&&(i.default={duration:kt(t,12),flags:kt(t,20)})})),e}function Pt(t){const e=t.subarray(8),s=e.subarray(86),i=Rt(e.subarray(4,8));let r=i;const n="enca"===i||"encv"===i;if(n){const t=_t(e,[i])[0];_t(t.subarray("enca"===i?28:78),["sinf"]).forEach((t=>{const e=_t(t,["schm"])[0];if(e){const s=Rt(e.subarray(4,8));if("cbcs"===s||"cenc"===s){const e=_t(t,["frma"])[0];e&&(r=Rt(e))}}}))}switch(r){case"avc1":case"avc2":case"avc3":case"avc4":{const t=_t(s,["avcC"])[0];r+="."+Ft(t[1])+Ft(t[2])+Ft(t[3]);break}case"mp4a":{const t=_t(e,[i])[0],s=_t(t.subarray(28),["esds"])[0];if(s&&s.length>12){let t=4;if(3!==s[t++])break;t=Mt(s,t),t+=2;const e=s[t++];if(128&e&&(t+=2),64&e&&(t+=s[t++]),4!==s[t++])break;t=Mt(s,t);const i=s[t++];if(64!==i)break;if(r+="."+Ft(i),t+=12,5!==s[t++])break;t=Mt(s,t);const n=s[t++];let a=(248&n)>>3;31===a&&(a+=1+((7&n)<<3)+((224&s[t])>>5)),r+="."+a}break}case"hvc1":case"hev1":{const t=_t(s,["hvcC"])[0],e=t[1],i=["","A","B","C"][e>>6],n=31&e,a=kt(t,2),o=(32&e)>>5?"H":"L",l=t[12],h=t.subarray(6,12);r+="."+i+n,r+="."+a.toString(16).toUpperCase(),r+="."+o+l;let d="";for(let t=h.length;t--;){const e=h[t];if(e||d){d="."+e.toString(16).toUpperCase()+d}}r+=d;break}case"dvh1":case"dvhe":{const t=_t(s,["dvcC"])[0],e=t[2]>>1&127,i=t[2]<<5&32|t[3]>>3&31;r+="."+Ot(e)+"."+Ot(i);break}case"vp09":{const t=_t(s,["vpcC"])[0],e=t[4],i=t[5],n=t[6]>>4&15;r+="."+Ot(e)+"."+Ot(i)+"."+Ot(n);break}case"av01":{const t=_t(s,["av1C"])[0],e=t[1]>>>5,i=31&t[1],n=t[2]>>>7?"H":"M",a=(64&t[2])>>6,o=(32&t[2])>>5,l=2===e&&a?o?12:10:a?10:8,h=(16&t[2])>>4,d=(8&t[2])>>3,c=(4&t[2])>>2,u=3&t[2],f=1,g=1,m=1,p=0;r+="."+e+"."+Ot(i)+n+"."+Ot(l)+"."+h+"."+d+c+u+"."+Ot(f)+"."+Ot(g)+"."+Ot(m)+"."+p;break}}return{codec:r,encrypted:n}}function Mt(t,e){const s=e+5;for(;128&t[e++]&&e{const l=o.byteOffset-8;_t(o,["traf"]).map((o=>{const h=_t(o,["tfdt"]).map((t=>{const e=t[0];let s=kt(t,4);return 1===e&&(s*=Math.pow(2,32),s+=kt(t,8)),s/r}))[0];return void 0!==h&&(t=h),_t(o,["tfhd"]).map((h=>{const d=kt(h,4),c=16777215&kt(h,0);let u=0;const f=!!(16&c);let g=0;const m=!!(32&c);let p=8;d===n&&(!!(1&c)&&(p+=8),!!(2&c)&&(p+=4),!!(8&c)&&(u=kt(h,p),p+=4),f&&(g=kt(h,p),p+=4),m&&(p+=4),"video"===e.type&&(a=function(t){if(!t)return!1;const e=t.indexOf("."),s=e<0?t:t.substring(0,e);return"hvc1"===s||"hev1"===s||"dvh1"===s||"dvhe"===s}(e.codec)),_t(o,["trun"]).map((n=>{const o=n[0],h=16777215&kt(n,0),d=!!(1&h);let c=0;const f=!!(4&h),m=!!(256&h);let p=0;const v=!!(512&h);let y=0;const E=!!(1024&h),T=!!(2048&h);let S=0;const L=kt(n,4);let A=8;d&&(c=kt(n,A),A+=4),f&&(A+=4);let R=c+l;for(let l=0;l>1&63;return 39===t||40===t}return 6===(31&e)}function Kt(t,e,s,i){const r=Ht(t);let n=0;n+=e;let a=0,o=0,l=0;for(;n=r.length)break;l=r[n++],a+=l}while(255===l);o=0;do{if(n>=r.length)break;l=r[n++],o+=l}while(255===l);const t=r.length-n;let e=n;if(ot){A.error(`Malformed SEI payload. ${o} is too small, only ${t} bytes left to parse.`);break}if(4===a){if(181===r[e++]){const t=bt(r,e);if(e+=2,49===t){const t=kt(r,e);if(e+=4,1195456820===t){const t=r[e++];if(3===t){const n=r[e++],o=64&n,l=o?2+3*(31&n):0,h=new Uint8Array(l);if(o){h[0]=n;for(let t=1;t16){const t=[];for(let s=0;s<16;s++){const i=r[e++].toString(16);t.push(1==i.length?"0"+i:i),3!==s&&5!==s&&7!==s&&9!==s||t.push("-")}const n=o-16,l=new Uint8Array(n);for(let t=0;t0?(n=new Uint8Array(4),e.length>0&&new DataView(n.buffer).setUint32(0,e.length,!1)):n=new Uint8Array;const a=new Uint8Array(4);return s&&s.byteLength>0&&new DataView(a.buffer).setUint32(0,s.byteLength,!1),function(t,...e){const s=e.length;let i=8,r=s;for(;r--;)i+=e[r].byteLength;const n=new Uint8Array(i);for(n[0]=i>>24&255,n[1]=i>>16&255,n[2]=i>>8&255,n[3]=255&i,n.set(t,4),r=0,i=8;r>>24;if(0!==r&&1!==r)return{offset:s,size:e};const n=t.buffer,a=Tt(new Uint8Array(n,s+12,16)),o=t.getUint32(28);let l=null,h=null;if(0===r){if(e-32>8*(15-s)&255;return e}(t);return new jt(this.method,this.uri,"identity",this.keyFormatVersions,e)}const e=U(this.uri);if(e)switch(this.keyFormat){case Y:this.pssh=e,e.length>=22&&(this.keyId=e.subarray(e.length-22,e.length-6));break;case V:{const t=new Uint8Array([154,4,240,121,152,64,66,134,171,146,230,91,224,136,95,149]);this.pssh=Vt(t,null,e);const s=new Uint16Array(e.buffer,e.byteOffset,e.byteLength/2),i=String.fromCharCode.apply(null,Array.from(s)),r=i.substring(i.indexOf("<"),i.length),n=(new DOMParser).parseFromString(r,"text/xml").getElementsByTagName("KID")[0];if(n){const t=n.childNodes[0]?n.childNodes[0].nodeValue:n.getAttribute("VALUE");if(t){const e=N(t).subarray(0,16);!function(t){const e=function(t,e,s){const i=t[e];t[e]=t[s],t[s]=i};e(t,0,3),e(t,1,2),e(t,4,5),e(t,6,7)}(e),this.keyId=e}}break}default:{let t=e.subarray(0,16);if(16!==t.length){const e=new Uint8Array(16);e.set(t,16-t.length),t=e}this.keyId=t;break}}if(!this.keyId||16!==this.keyId.byteLength){let t=Wt[this.uri];if(!t){const e=Object.keys(Wt).length%Number.MAX_SAFE_INTEGER;t=new Uint8Array(16);new DataView(t.buffer,12,4).setUint32(0,e),Wt[this.uri]=t}this.keyId=t}return this}}const qt=/\{\$([a-zA-Z0-9-_]+)\}/g;function Xt(t){return qt.test(t)}function zt(t,e,s){if(null!==t.variableList||t.hasVariableRefs)for(let i=s.length;i--;){const r=s[i],n=e[r];n&&(e[r]=Qt(t,n))}}function Qt(t,e){if(null!==t.variableList||t.hasVariableRefs){const s=t.variableList;return e.replace(qt,(e=>{const i=e.substring(2,e.length-1),r=null==s?void 0:s[i];return void 0===r?(t.playlistParsingError||(t.playlistParsingError=new Error(`Missing preceding EXT-X-DEFINE tag for Variable Reference: "${i}"`)),e):r}))}return e}function Jt(t,e,s){let i,r,n=t.variableList;if(n||(t.variableList=n={}),"QUERYPARAM"in e){i=e.QUERYPARAM;try{const t=new self.URL(s).searchParams;if(!t.has(i))throw new Error(`"${i}" does not match any query parameter in URI: "${s}"`);r=t.get(i)}catch(e){t.playlistParsingError||(t.playlistParsingError=new Error(`EXT-X-DEFINE QUERYPARAM: ${e.message}`))}}else i=e.NAME,r=e.VALUE;i in n?t.playlistParsingError||(t.playlistParsingError=new Error(`EXT-X-DEFINE duplicate Variable Name declarations: "${i}"`)):n[i]=r||""}function Zt(t,e,s){const i=e.IMPORT;if(s&&i in s){let e=t.variableList;e||(t.variableList=e={}),e[i]=s[i]}else t.playlistParsingError||(t.playlistParsingError=new Error(`EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "${i}"`))}function te(t=!0){if("undefined"==typeof self)return;return(t||!self.MediaSource)&&self.ManagedMediaSource||self.MediaSource||self.WebKitMediaSource}const ee={audio:{a3ds:1,"ac-3":.95,"ac-4":1,alac:.9,alaw:1,dra1:1,"dts+":1,"dts-":1,dtsc:1,dtse:1,dtsh:1,"ec-3":.9,enca:1,fLaC:.9,flac:.9,FLAC:.9,g719:1,g726:1,m4ae:1,mha1:1,mha2:1,mhm1:1,mhm2:1,mlpa:1,mp4a:1,"raw ":1,Opus:1,opus:1,samr:1,sawb:1,sawp:1,sevc:1,sqcp:1,ssmv:1,twos:1,ulaw:1},video:{avc1:1,avc2:1,avc3:1,avc4:1,avcp:1,av01:.8,drac:1,dva1:1,dvav:1,dvh1:.7,dvhe:.7,encv:1,hev1:.75,hvc1:.75,mjp2:1,mp4v:1,mvc1:1,mvc2:1,mvc3:1,mvc4:1,resv:1,rv60:1,s263:1,svc1:1,svc2:1,"vc-1":1,vp08:1,vp09:.9},text:{stpp:1,wvtt:1}};function se(t,e,s=!0){return!t.split(",").some((t=>!ie(t,e,s)))}function ie(t,e,s=!0){var i;const r=te(s);return null!=(i=null==r?void 0:r.isTypeSupported(re(t,e)))&&i}function re(t,e){return`${e}/mp4;codecs="${t}"`}function ne(t){if(t){const e=t.substring(0,4);return ee.video[e]}return 2}function ae(t){return t.split(",").reduce(((t,e)=>{const s=ee.video[e];return s?(2*s+t)/(t?3:2):(ee.audio[e]+t)/(t?2:1)}),0)}const oe={};const le=/flac|opus/i;function he(t,e=!0){return t.replace(le,(t=>function(t,e=!0){if(oe[t])return oe[t];const s={flac:["flac","fLaC","FLAC"],opus:["opus","Opus"]}[t];for(let i=0;i0&&i.length({id:t.attrs.AUDIO,audioCodec:t.audioCodec}))),SUBTITLES:n.map((t=>({id:t.attrs.SUBTITLES,textCodec:t.textCodec}))),"CLOSED-CAPTIONS":[]};let o=0;for(ue.lastIndex=0;null!==(i=ue.exec(t));){const t=new k(i[1]),n=t.TYPE;if(n){const i=a[n],l=r[n]||[];r[n]=l,zt(s,t,["URI","GROUP-ID","LANGUAGE","ASSOC-LANGUAGE","STABLE-RENDITION-ID","NAME","INSTREAM-ID","CHARACTERISTICS","CHANNELS"]);const h=t.LANGUAGE,d=t["ASSOC-LANGUAGE"],c=t.CHANNELS,u=t.CHARACTERISTICS,f=t["INSTREAM-ID"],g={attrs:t,bitrate:0,id:o++,groupId:t["GROUP-ID"]||"",name:t.NAME||h||"",type:n,default:t.bool("DEFAULT"),autoselect:t.bool("AUTOSELECT"),forced:t.bool("FORCED"),lang:h,url:t.URI?pe.resolve(t.URI,e):""};if(d&&(g.assocLang=d),c&&(g.channels=c),u&&(g.characteristics=u),f&&(g.instreamId=f),null!=i&&i.length){const t=pe.findGroup(i,g.groupId)||i[0];Te(g,t,"audioCodec"),Te(g,t,"textCodec")}l.push(g)}}return r}static parseLevelPlaylist(t,e,s,i,r,n){const a=new O(e),o=a.fragments;let l,h,d,c=null,g=0,m=0,p=0,v=0,y=null,E=new M(i,e),T=-1,S=!1,L=null;for(ge.lastIndex=0,a.m3u8=t,a.hasVariableRefs=Xt(t);null!==(l=ge.exec(t));){S&&(S=!1,E=new M(i,e),E.start=p,E.sn=g,E.cc=v,E.level=s,c&&(E.initSegment=c,E.rawProgramDateTime=c.rawProgramDateTime,c.rawProgramDateTime=null,L&&(E.setByteRange(L),L=null)));const t=l[1];if(t){E.duration=parseFloat(t);const e=(" "+l[2]).slice(1);E.title=e||null,E.tagList.push(e?["INF",t,e]:["INF",t])}else if(l[3]){if(f(E.duration)){E.start=p,d&&Ae(E,d,a),E.sn=g,E.level=s,E.cc=v,o.push(E);const t=(" "+l[3]).slice(1);E.relurl=Qt(a,t),Se(E,y),y=E,p+=E.duration,g++,m=0,S=!0}}else if(l[4]){const t=(" "+l[4]).slice(1);y?E.setByteRange(t,y):E.setByteRange(t)}else if(l[5])E.rawProgramDateTime=(" "+l[5]).slice(1),E.tagList.push(["PROGRAM-DATE-TIME",E.rawProgramDateTime]),-1===T&&(T=o.length);else{if(l=l[0].match(me),!l){A.warn("No matches on slow regex match for level playlist!");continue}for(h=1;h0&&t.bool("CAN-SKIP-DATERANGES"),a.partHoldBack=t.optionalFloat("PART-HOLD-BACK",0),a.holdBack=t.optionalFloat("HOLD-BACK",0);break}case"PART-INF":{const t=new k(r);a.partTarget=t.decimalFloatingPoint("PART-TARGET");break}case"PART":{let t=a.partList;t||(t=a.partList=[]);const s=m>0?t[t.length-1]:void 0,i=m++,n=new k(r);zt(a,n,["BYTERANGE","URI"]);const o=new F(n,E,e,i,s);t.push(o),E.duration+=o.duration;break}case"PRELOAD-HINT":{const t=new k(r);zt(a,t,["URI"]),a.preloadHint=t;break}case"RENDITION-REPORT":{const t=new k(r);zt(a,t,["URI"]),a.renditionReports=a.renditionReports||[],a.renditionReports.push(t);break}default:A.warn(`line parsed but not handled: ${l}`)}}}y&&!y.relurl?(o.pop(),p-=y.duration,a.partList&&(a.fragmentHint=y)):a.partList&&(Se(E,y),E.cc=v,a.fragmentHint=E,d&&Ae(E,d,a));const R=o.length,b=o[0],w=o[R-1];if(p+=a.skippedSegments*a.targetduration,p>0&&R&&w){a.averagetargetduration=p/R;const t=w.sn;a.endSN="initSegment"!==t?t:0,a.live||(w.endList=!0),b&&(a.startCC=b.cc)}else a.endSN=0,a.startCC=0;return a.fragmentHint&&(p+=a.fragmentHint.duration),a.totalduration=p,a.endCC=v,T>0&&function(t,e){let s=t[e];for(let i=e;i--;){const e=t[i];if(!e)return;e.programDateTime=s.programDateTime-1e3*e.duration,s=e}}(o,T),a}}function ve(t,e,s){var i,r;const n=new k(t);zt(s,n,["KEYFORMAT","KEYFORMATVERSIONS","URI","IV","URI"]);const a=null!=(i=n.METHOD)?i:"",o=n.URI,l=n.hexadecimalInteger("IV"),h=n.KEYFORMATVERSIONS,d=null!=(r=n.KEYFORMAT)?r:"identity";o&&n.IV&&!l&&A.error(`Invalid IV: ${n.IV}`);const c=o?pe.resolve(o,e):"",u=(h||"1").split("/").map(Number).filter(Number.isFinite);return new jt(a,c,d,u,l)}function ye(t){const e=new k(t).decimalFloatingPoint("TIME-OFFSET");return f(e)?e:null}function Ee(t,e){let s=(t||"").split(/[ ,]+/).filter((t=>t));["video","audio","text"].forEach((t=>{const i=s.filter((e=>function(t,e){const s=ee[e];return!!s&&!!s[t.slice(0,4)]}(e,t)));i.length&&(e[`${t}Codec`]=i.join(","),s=s.filter((t=>-1===i.indexOf(t))))})),e.unknownCodecs=s}function Te(t,e,s){const i=e[s];i&&(t[s]=i)}function Se(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),f(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}function Le(t,e,s,i){t.relurl=e.URI,e.BYTERANGE&&t.setByteRange(e.BYTERANGE),t.level=s,t.sn="initSegment",i&&(t.levelkeys=i),t.initSegment=null}function Ae(t,e,s){t.levelkeys=e;const{encryptedFragments:i}=s;i.length&&i[i.length-1].levelkeys===e||!Object.keys(e).some((t=>e[t].isCommonEncryption))||i.push(t)}var Re="manifest",be="level",ke="audioTrack",we="subtitleTrack",De="main",Ie="audio",_e="subtitle";function Ce(t){const{type:e}=t;switch(e){case ke:return Ie;case we:return _e;default:return De}}function xe(t,e){let s=t.url;return void 0!==s&&0!==s.indexOf("data:")||(s=e.url),s}class Pe{constructor(t){this.hls=void 0,this.loaders=Object.create(null),this.variableList=null,this.hls=t,this.registerListeners()}startLoad(t){}stopLoad(){this.destroyInternalLoaders()}registerListeners(){const{hls:t}=this;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.LEVEL_LOADING,this.onLevelLoading,this),t.on(p.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(p.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)}unregisterListeners(){const{hls:t}=this;t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.LEVEL_LOADING,this.onLevelLoading,this),t.off(p.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(p.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)}createInternalLoader(t){const e=this.hls.config,s=e.pLoader,i=e.loader,r=new(s||i)(e);return this.loaders[t.type]=r,r}getInternalLoader(t){return this.loaders[t.type]}resetInternalLoader(t){this.loaders[t]&&delete this.loaders[t]}destroyInternalLoaders(){for(const t in this.loaders){const e=this.loaders[t];e&&e.destroy(),this.resetInternalLoader(t)}}destroy(){this.variableList=null,this.unregisterListeners(),this.destroyInternalLoaders()}onManifestLoading(t,e){const{url:s}=e;this.variableList=null,this.load({id:null,level:0,responseType:"text",type:Re,url:s,deliveryDirectives:null})}onLevelLoading(t,e){const{id:s,level:i,pathwayId:r,url:n,deliveryDirectives:a}=e;this.load({id:s,level:i,pathwayId:r,responseType:"text",type:be,url:n,deliveryDirectives:a})}onAudioTrackLoading(t,e){const{id:s,groupId:i,url:r,deliveryDirectives:n}=e;this.load({id:s,groupId:i,level:null,responseType:"text",type:ke,url:r,deliveryDirectives:n})}onSubtitleTrackLoading(t,e){const{id:s,groupId:i,url:r,deliveryDirectives:n}=e;this.load({id:s,groupId:i,level:null,responseType:"text",type:we,url:r,deliveryDirectives:n})}load(t){var e;const s=this.hls.config;let i,r=this.getInternalLoader(t);if(r){const e=r.context;if(e&&e.url===t.url&&e.level===t.level)return void A.trace("[playlist-loader]: playlist request ongoing");A.log(`[playlist-loader]: aborting previous loader for type: ${t.type}`),r.abort()}if(i=t.type===Re?s.manifestLoadPolicy.default:u({},s.playlistLoadPolicy.default,{timeoutRetry:null,errorRetry:null}),r=this.createInternalLoader(t),f(null==(e=t.deliveryDirectives)?void 0:e.part)){let e;if(t.type===be&&null!==t.level?e=this.hls.levels[t.level].details:t.type===ke&&null!==t.id?e=this.hls.audioTracks[t.id].details:t.type===we&&null!==t.id&&(e=this.hls.subtitleTracks[t.id].details),e){const t=e.partTarget,s=e.targetduration;if(t&&s){const e=1e3*Math.max(3*t,.8*s);i=u({},i,{maxTimeToFirstByteMs:Math.min(e,i.maxTimeToFirstByteMs),maxLoadTimeMs:Math.min(e,i.maxTimeToFirstByteMs)})}}}const n=i.errorRetry||i.timeoutRetry||{},a={loadPolicy:i,timeout:i.maxLoadTimeMs,maxRetry:n.maxNumRetry||0,retryDelay:n.retryDelayMs||0,maxRetryDelay:n.maxRetryDelayMs||0},o={onSuccess:(t,e,s,i)=>{const r=this.getInternalLoader(s);this.resetInternalLoader(s.type);const n=t.data;0===n.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),pe.isMediaPlaylist(n)?this.handleTrackOrLevelPlaylist(t,e,s,i||null,r):this.handleMasterPlaylist(t,e,s,i)):this.handleManifestParsingError(t,s,new Error("no EXTM3U delimiter"),i||null,e)},onError:(t,e,s,i)=>{this.handleNetworkError(e,s,!1,t,i)},onTimeout:(t,e,s)=>{this.handleNetworkError(e,s,!0,void 0,t)}};r.load(t,a,o)}handleMasterPlaylist(t,e,s,i){const r=this.hls,n=t.data,a=xe(t,s),o=pe.parseMasterPlaylist(n,a);if(o.playlistParsingError)return void this.handleManifestParsingError(t,s,o.playlistParsingError,i,e);const{contentSteering:l,levels:h,sessionData:d,sessionKeys:c,startTimeOffset:u,variableList:f}=o;this.variableList=f;const{AUDIO:g=[],SUBTITLES:m,"CLOSED-CAPTIONS":v}=pe.parseMasterPlaylistMedia(n,a,o);if(g.length){g.some((t=>!t.url))||!h[0].audioCodec||h[0].attrs.AUDIO||(A.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),g.unshift({type:"main",name:"main",groupId:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new k({}),bitrate:0,url:""}))}r.trigger(p.MANIFEST_LOADED,{levels:h,audioTracks:g,subtitles:m,captions:v,contentSteering:l,url:a,stats:e,networkDetails:i,sessionData:d,sessionKeys:c,startTimeOffset:u,variableList:f})}handleTrackOrLevelPlaylist(t,e,s,i,r){const n=this.hls,{id:a,level:o,type:l}=s,h=xe(t,s),d=f(o)?o:f(a)?a:0,c=Ce(s),u=pe.parseLevelPlaylist(t.data,h,d,c,0,this.variableList);if(l===Re){const t={attrs:new k({}),bitrate:0,details:u,name:"",url:h};n.trigger(p.MANIFEST_LOADED,{levels:[t],audioTracks:[],url:h,stats:e,networkDetails:i,sessionData:null,sessionKeys:null,contentSteering:null,startTimeOffset:null,variableList:null})}e.parsing.end=performance.now(),s.levelDetails=u,this.handlePlaylistLoaded(u,t,e,s,i,r)}handleManifestParsingError(t,e,s,i,r){this.hls.trigger(p.ERROR,{type:v.NETWORK_ERROR,details:y.MANIFEST_PARSING_ERROR,fatal:e.type===Re,url:t.url,err:s,error:s,reason:s.message,response:t,context:e,networkDetails:i,stats:r})}handleNetworkError(t,e,s=!1,i,r){let n=`A network ${s?"timeout":"error"+(i?" (status "+i.code+")":"")} occurred while loading ${t.type}`;t.type===be?n+=`: ${t.level} id: ${t.id}`:t.type!==ke&&t.type!==we||(n+=` id: ${t.id} group-id: "${t.groupId}"`);const a=new Error(n);A.warn(`[playlist-loader]: ${n}`);let o=y.UNKNOWN,l=!1;const d=this.getInternalLoader(t);switch(t.type){case Re:o=s?y.MANIFEST_LOAD_TIMEOUT:y.MANIFEST_LOAD_ERROR,l=!0;break;case be:o=s?y.LEVEL_LOAD_TIMEOUT:y.LEVEL_LOAD_ERROR,l=!1;break;case ke:o=s?y.AUDIO_TRACK_LOAD_TIMEOUT:y.AUDIO_TRACK_LOAD_ERROR,l=!1;break;case we:o=s?y.SUBTITLE_TRACK_LOAD_TIMEOUT:y.SUBTITLE_LOAD_ERROR,l=!1}d&&this.resetInternalLoader(t.type);const c={type:v.NETWORK_ERROR,details:o,fatal:l,url:t.url,loader:d,context:t,error:a,networkDetails:e,stats:r};if(i){const s=(null==e?void 0:e.url)||t.url;c.response=h({url:s,data:void 0},i)}this.hls.trigger(p.ERROR,c)}handlePlaylistLoaded(t,e,s,i,r,n){const a=this.hls,{type:o,level:l,id:h,groupId:d,deliveryDirectives:c}=i,u=xe(e,i),f=Ce(i),g="number"==typeof i.level&&f===De?l:void 0;if(!t.fragments.length){const t=new Error("No Segments found in Playlist");return void a.trigger(p.ERROR,{type:v.NETWORK_ERROR,details:y.LEVEL_EMPTY_ERROR,fatal:!1,url:u,error:t,reason:t.message,response:e,context:i,level:g,parent:f,networkDetails:r,stats:s})}t.targetduration||(t.playlistParsingError=new Error("Missing Target Duration"));const m=t.playlistParsingError;if(m)a.trigger(p.ERROR,{type:v.NETWORK_ERROR,details:y.LEVEL_PARSING_ERROR,fatal:!1,url:u,error:m,reason:m.message,response:e,context:i,level:g,parent:f,networkDetails:r,stats:s});else switch(t.live&&n&&(n.getCacheAge&&(t.ageHeader=n.getCacheAge()||0),n.getCacheAge&&!isNaN(t.ageHeader)||(t.ageHeader=0)),o){case Re:case be:a.trigger(p.LEVEL_LOADED,{details:t,level:g||0,id:h||0,stats:s,networkDetails:r,deliveryDirectives:c});break;case ke:a.trigger(p.AUDIO_TRACK_LOADED,{details:t,id:h||0,groupId:d||"",stats:s,networkDetails:r,deliveryDirectives:c});break;case we:a.trigger(p.SUBTITLE_TRACK_LOADED,{details:t,id:h||0,groupId:d||"",stats:s,networkDetails:r,deliveryDirectives:c})}}}function Me(t,e){let s;try{s=new Event("addtrack")}catch(t){s=document.createEvent("Event"),s.initEvent("addtrack",!1,!1)}s.track=t,e.dispatchEvent(s)}function Fe(t,e){const s=t.mode;if("disabled"===s&&(t.mode="hidden"),t.cues&&!t.cues.getCueById(e.id))try{if(t.addCue(e),!t.cues.getCueById(e.id))throw new Error(`addCue is failed for: ${e}`)}catch(s){A.debug(`[texttrack-utils]: ${s}`);try{const s=new self.TextTrackCue(e.startTime,e.endTime,e.text);s.id=e.id,t.addCue(s)}catch(t){A.debug(`[texttrack-utils]: Legacy TextTrackCue fallback failed: ${t}`)}}"disabled"===s&&(t.mode=s)}function Oe(t){const e=t.mode;if("disabled"===e&&(t.mode="hidden"),t.cues)for(let e=t.cues.length;e--;)t.removeCue(t.cues[e]);"disabled"===e&&(t.mode=e)}function Ne(t,e,s,i){const r=t.mode;if("disabled"===r&&(t.mode="hidden"),t.cues&&t.cues.length>0){const r=function(t,e,s){const i=[],r=function(t,e){if(et[s].endTime)return-1;let i=0,r=s;for(;i<=r;){const n=Math.floor((r+i)/2);if(et[n].startTime&&i-1)for(let n=r,a=t.length;n=e&&r.endTime<=s)i.push(r);else if(r.startTime>s)return i}return i}(t.cues,e,s);for(let e=0;e{const t=Ke();try{t&&new t(0,Number.POSITIVE_INFINITY,"")}catch(t){return Number.MAX_VALUE}return Number.POSITIVE_INFINITY})();function Ye(t,e){return t.getTime()/1e3-e}class We{constructor(t){this.hls=void 0,this.id3Track=null,this.media=null,this.dateRangeCuesAppended={},this.hls=t,this._registerListeners()}destroy(){this._unregisterListeners(),this.id3Track=null,this.media=null,this.dateRangeCuesAppended={},this.hls=null}_registerListeners(){const{hls:t}=this;t.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.FRAG_PARSING_METADATA,this.onFragParsingMetadata,this),t.on(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(p.LEVEL_UPDATED,this.onLevelUpdated,this)}_unregisterListeners(){const{hls:t}=this;t.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.FRAG_PARSING_METADATA,this.onFragParsingMetadata,this),t.off(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(p.LEVEL_UPDATED,this.onLevelUpdated,this)}onMediaAttached(t,e){this.media=e.media}onMediaDetaching(){this.id3Track&&(Oe(this.id3Track),this.id3Track=null,this.media=null,this.dateRangeCuesAppended={})}onManifestLoading(){this.dateRangeCuesAppended={}}createTrack(t){const e=this.getID3Track(t.textTracks);return e.mode="hidden",e}getID3Track(t){if(this.media){for(let e=0;eVe&&(i=Ve);i-s<=0&&(i=s+.25);for(let t=0;tt.type===Be&&o:"video"===i?t=>t.type===Ge&&a:t=>t.type===Be&&o||t.type===Ge&&a,Ne(r,e,s,t)}}onLevelUpdated(t,{details:e}){if(!this.media||!e.hasProgramDateTime||!this.hls.config.enableDateRangeMetadataCues)return;const{dateRangeCuesAppended:s,id3Track:i}=this,{dateRanges:r}=e,n=Object.keys(r);if(i){const t=Object.keys(s).filter((t=>!n.includes(t)));for(let e=t.length;e--;){const r=t[e];Object.keys(s[r].cues).forEach((t=>{i.removeCue(s[r].cues[t])})),delete s[r]}}const a=e.fragments[e.fragments.length-1];if(0===n.length||!f(null==a?void 0:a.programDateTime))return;this.id3Track||(this.id3Track=this.createTrack(this.media));const o=a.programDateTime/1e3-a.start,l=Ke();for(let t=0;t{if(e!==i.id){const s=r[e];if(s.class===i.class&&s.startDate>i.startDate&&(!t||i.startDatethis.timeupdate(),this.hls=t,this.config=t.config,this.registerListeners()}get latency(){return this._latency||0}get maxLatency(){const{config:t,levelDetails:e}=this;return void 0!==t.liveMaxLatencyDuration?t.liveMaxLatencyDuration:e?t.liveMaxLatencyDurationCount*e.targetduration:0}get targetLatency(){const{levelDetails:t}=this;if(null===t)return null;const{holdBack:e,partHoldBack:s,targetduration:i}=t,{liveSyncDuration:r,liveSyncDurationCount:n,lowLatencyMode:a}=this.config,o=this.hls.userConfig;let l=a&&s||e;(o.liveSyncDuration||o.liveSyncDurationCount||0===l)&&(l=void 0!==r?r:n*i);const h=i;return l+Math.min(1*this.stallCount,h)}get liveSyncPosition(){const t=this.estimateLiveEdge(),e=this.targetLatency,s=this.levelDetails;if(null===t||null===e||null===s)return null;const i=s.edge,r=t-e-this.edgeStalled,n=i-s.totalduration,a=i-(this.config.lowLatencyMode&&s.partTarget||s.targetduration);return Math.min(Math.max(n,r),a)}get drift(){const{levelDetails:t}=this;return null===t?1:t.drift}get edgeStalled(){const{levelDetails:t}=this;if(null===t)return 0;const e=3*(this.config.lowLatencyMode&&t.partTarget||t.targetduration);return Math.max(t.age-e,0)}get forwardBufferLength(){const{media:t,levelDetails:e}=this;if(!t||!e)return 0;const s=t.buffered.length;return(s?t.buffered.end(s-1):e.edge)-this.currentTime}destroy(){this.unregisterListeners(),this.onMediaDetaching(),this.levelDetails=null,this.hls=this.timeupdateHandler=null}registerListeners(){this.hls.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),this.hls.on(p.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(p.LEVEL_UPDATED,this.onLevelUpdated,this),this.hls.on(p.ERROR,this.onError,this)}unregisterListeners(){this.hls.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),this.hls.off(p.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(p.LEVEL_UPDATED,this.onLevelUpdated,this),this.hls.off(p.ERROR,this.onError,this)}onMediaAttached(t,e){this.media=e.media,this.media.addEventListener("timeupdate",this.timeupdateHandler)}onMediaDetaching(){this.media&&(this.media.removeEventListener("timeupdate",this.timeupdateHandler),this.media=null)}onManifestLoading(){this.levelDetails=null,this._latency=null,this.stallCount=0}onLevelUpdated(t,{details:e}){this.levelDetails=e,e.advanced&&this.timeupdate(),!e.live&&this.media&&this.media.removeEventListener("timeupdate",this.timeupdateHandler)}onError(t,e){var s;e.details===y.BUFFER_STALLED_ERROR&&(this.stallCount++,null!=(s=this.levelDetails)&&s.live&&A.warn("[playback-rate-controller]: Stall detected, adjusting target latency"))}timeupdate(){const{media:t,levelDetails:e}=this;if(!t||!e)return;this.currentTime=t.currentTime;const s=this.computeLatency();if(null===s)return;this._latency=s;const{lowLatencyMode:i,maxLiveSyncPlaybackRate:r}=this.config;if(!i||1===r||!e.live)return;const n=this.targetLatency;if(null===n)return;const a=s-n;if(a.05&&this.forwardBufferLength>1){const e=Math.min(2,Math.max(1,r)),s=Math.round(2/(1+Math.exp(-.75*a-this.edgeStalled))*20)/20;t.playbackRate=Math.min(e,Math.max(1,s))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}estimateLiveEdge(){const{levelDetails:t}=this;return null===t?null:t.edge+t.age}computeLatency(){const t=this.estimateLiveEdge();return null===t?null:t-this.currentTime}}const qe=["NONE","TYPE-0","TYPE-1",null];const Xe=["SDR","PQ","HLG"];var ze="",Qe="YES",Je="v2";function Ze(t){const{canSkipUntil:e,canSkipDateRanges:s,age:i}=t;return e&&i!!t)).map((t=>t.substring(0,4))).join(","),this.addGroupId("audio",t.attrs.AUDIO),this.addGroupId("text",t.attrs.SUBTITLES)}get maxBitrate(){return Math.max(this.realBitrate,this.bitrate)}get averageBitrate(){return this._avgBitrate||this.realBitrate||this.bitrate}get attrs(){return this._attrs[0]}get codecs(){return this.attrs.CODECS||""}get pathwayId(){return this.attrs["PATHWAY-ID"]||"."}get videoRange(){return this.attrs["VIDEO-RANGE"]||"SDR"}get score(){return this.attrs.optionalFloat("SCORE",0)}get uri(){return this.url[0]||""}hasAudioGroup(t){return ss(this._audioGroups,t)}hasSubtitleGroup(t){return ss(this._subtitleGroups,t)}get audioGroups(){return this._audioGroups}get subtitleGroups(){return this._subtitleGroups}addGroupId(t,e){if(e)if("audio"===t){let t=this._audioGroups;t||(t=this._audioGroups=[]),-1===t.indexOf(e)&&t.push(e)}else if("text"===t){let t=this._subtitleGroups;t||(t=this._subtitleGroups=[]),-1===t.indexOf(e)&&t.push(e)}}get urlId(){return 0}set urlId(t){}get audioGroupIds(){return this.audioGroups?[this.audioGroupId]:void 0}get textGroupIds(){return this.subtitleGroups?[this.textGroupId]:void 0}get audioGroupId(){var t;return null==(t=this.audioGroups)?void 0:t[0]}get textGroupId(){var t;return null==(t=this.subtitleGroups)?void 0:t[0]}addFallback(){}}function ss(t,e){return!(!e||!t)&&-1!==t.indexOf(e)}function is(t,e){const s=e.startPTS;if(f(s)){let i,r=0;e.sn>t.sn?(r=s-t.start,i=t):(r=t.start-s,i=e),i.duration!==r&&(i.duration=r)}else if(e.sn>t.sn){t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration}else e.start=Math.max(t.start-e.duration,0)}function rs(t,e,s,i,r,n){i-s<=0&&(A.warn("Fragment should have a positive duration",e),i=s+e.duration,n=r+e.duration);let a=s,o=i;const l=e.startPTS,h=e.endPTS;if(f(l)){const t=Math.abs(l-s);f(e.deltaPTS)?e.deltaPTS=Math.max(t,e.deltaPTS):e.deltaPTS=t,a=Math.max(s,l),s=Math.min(s,l),r=Math.min(r,e.startDTS),o=Math.min(i,h),i=Math.max(i,h),n=Math.max(n,e.endDTS)}const d=s-e.start;0!==e.start&&(e.start=s),e.duration=i-e.start,e.startPTS=s,e.maxStartPTS=a,e.startDTS=r,e.endPTS=i,e.minEndPTS=o,e.endDTS=n;const c=e.sn;if(!t||ct.endSN)return 0;let u;const g=c-t.startSN,m=t.fragments;for(m[g]=e,u=g;u>0;u--)is(m[u],m[u-1]);for(u=g;u=0;t--){const e=i[t].initSegment;if(e){s=e;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;let r,n=0;if(function(t,e,s){const i=e.skippedSegments,r=Math.max(t.startSN,e.startSN)-e.startSN,n=(t.fragmentHint?1:0)+(i?e.endSN:Math.min(t.endSN,e.endSN))-e.startSN,a=e.startSN-t.startSN,o=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,l=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments;for(let t=r;t<=n;t++){const r=l[a+t];let n=o[t];i&&!n&&t{t.relurl&&(n=t.cc-i.cc),f(t.startPTS)&&f(t.endPTS)&&(i.start=i.startPTS=t.startPTS,i.startDTS=t.startDTS,i.maxStartPTS=t.maxStartPTS,i.endPTS=t.endPTS,i.endDTS=t.endDTS,i.minEndPTS=t.minEndPTS,i.duration=t.endPTS-t.startPTS,i.duration&&(r=i),e.PTSKnown=e.alignedSliding=!0),i.elementaryStreams=t.elementaryStreams,i.loader=t.loader,i.stats=t.stats,t.initSegment&&(i.initSegment=t.initSegment,s=t.initSegment)})),s){(e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments).forEach((t=>{var e;!t||t.initSegment&&t.initSegment.relurl!==(null==(e=s)?void 0:e.relurl)||(t.initSegment=s)}))}if(e.skippedSegments)if(e.deltaUpdateFailed=e.fragments.some((t=>!t)),e.deltaUpdateFailed){A.warn("[level-helper] Previous playlist missing segments skipped in delta playlist");for(let t=e.skippedSegments;t--;)e.fragments.shift();e.startSN=e.fragments[0].sn,e.startCC=e.fragments[0].cc}else e.canSkipDateRanges&&(e.dateRanges=function(t,e,s){const i=u({},t);s&&s.forEach((t=>{delete i[t]}));return Object.keys(e).forEach((t=>{const s=new D(e[t].attr,i[t]);s.isValid?i[t]=s:A.warn(`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(e[t].attr)}"`)})),i}(t.dateRanges,e.dateRanges,e.recentlyRemovedDateranges));const a=e.fragments;if(n){A.warn("discontinuity sliding from playlist, take drift into account");for(let t=0;t{e.elementaryStreams=t.elementaryStreams,e.stats=t.stats})),r?rs(e,r,r.startPTS,r.endPTS,r.startDTS,r.endDTS):as(t,e),a.length&&(e.totalduration=e.edge-a[0].start),e.driftStartTime=t.driftStartTime,e.driftStart=t.driftStart;const o=e.advancedDateTime;if(e.advanced&&o){const t=e.edge;e.driftStart||(e.driftStartTime=o,e.driftStart=t),e.driftEndTime=o,e.driftEnd=t}else e.driftEndTime=t.driftEndTime,e.driftEnd=t.driftEnd,e.advancedDateTime=t.advancedDateTime}function as(t,e){const s=e.startSN+e.skippedSegments-t.startSN,i=t.fragments;s<0||s>=i.length||os(e,i[s].start)}function os(t,e){if(e){const s=t.fragments;for(let i=t.skippedSegments;i{const{details:s}=t;null!=s&&s.fragments&&s.fragments.forEach((t=>{t.level=e}))}))}function cs(t){switch(t.details){case y.FRAG_LOAD_TIMEOUT:case y.KEY_LOAD_TIMEOUT:case y.LEVEL_LOAD_TIMEOUT:case y.MANIFEST_LOAD_TIMEOUT:return!0}return!1}function us(t,e){const s=cs(e);return t.default[(s?"timeout":"error")+"Retry"]}function fs(t,e){const s="linear"===t.backoff?1:Math.pow(2,e);return Math.min(s*t.retryDelayMs,t.maxRetryDelayMs)}function gs(t){return h(h({},t),{errorRetry:null,timeoutRetry:null})}function ms(t,e,s,i){if(!t)return!1;const r=null==i?void 0:i.code,n=e499)}(r)||!!s);return t.shouldRetry?t.shouldRetry(t,e,s,i,n):n}const ps=function(t,e){let s=0,i=t.length-1,r=null,n=null;for(;s<=i;){r=(s+i)/2|0,n=t[r];const a=e(n);if(a>0)s=r+1;else{if(!(a<0))return n;i=r-1}}return null};function vs(t,e,s=0,i=0,r=.005){let n=null;if(t){n=e[t.sn-e[0].sn+1]||null;const i=t.endDTS-s;i>0&&i<15e-7&&(s+=15e-7)}else 0===s&&0===e[0].start&&(n=e[0]);if(n&&((!t||t.level===n.level)&&0===ys(s,i,n)||function(t,e,s){if(e&&0===e.start&&e.level0){const i=e.tagList.reduce(((t,e)=>("INF"===e[0]&&(t+=parseFloat(e[1])),t)),s);return t.start<=i}return!1}(n,t,Math.min(r,i))))return n;const a=ps(e,ys.bind(null,s,i));return!a||a===t&&n?n:a}function ys(t=0,e=0,s){if(s.start<=t&&s.start+s.duration>t)return 0;const i=Math.min(e,s.duration+(s.deltaPTS?s.deltaPTS:0));return s.start+s.duration-i<=t?1:s.start-i>t&&s.start?-1:0}function Es(t,e,s){const i=1e3*Math.min(e,s.duration+(s.deltaPTS?s.deltaPTS:0));return(s.endProgramDateTime||0)-i>t}var Ts=0,Ss=2,Ls=3,As=5,Rs=0,bs=1,ks=2;class ws{constructor(t,e){this.hls=void 0,this.timer=-1,this.requestScheduled=-1,this.canLoad=!1,this.log=void 0,this.warn=void 0,this.log=A.log.bind(A,`${e}:`),this.warn=A.warn.bind(A,`${e}:`),this.hls=t}destroy(){this.clearTimer(),this.hls=this.log=this.warn=null}clearTimer(){-1!==this.timer&&(self.clearTimeout(this.timer),this.timer=-1)}startLoad(){this.canLoad=!0,this.requestScheduled=-1,this.loadPlaylist()}stopLoad(){this.canLoad=!1,this.clearTimer()}switchParams(t,e,s){const i=null==e?void 0:e.renditionReports;if(i){let r=-1;for(let s=0;s=0&&t>e.partTarget&&(a+=1)}const o=s&&Ze(s);return new ts(n,a>=0?a:void 0,o)}}}loadPlaylist(t){-1===this.requestScheduled&&(this.requestScheduled=self.performance.now())}shouldLoadPlaylist(t){return this.canLoad&&!!t&&!!t.url&&(!t.details||t.details.live)}shouldReloadPlaylist(t){return-1===this.timer&&-1===this.requestScheduled&&this.shouldLoadPlaylist(t)}playlistLoaded(t,e,s){const{details:i,stats:r}=e,n=self.performance.now(),a=r.loading.first?Math.max(0,n-r.loading.first):0;if(i.advancedDateTime=Date.now()-a,i.live||null!=s&&s.live){if(i.reloaded(s),s&&this.log(`live playlist ${t} ${i.advanced?"REFRESHED "+i.lastPartSn+"-"+i.lastPartIndex:i.updated?"UPDATED":"MISSED"}`),s&&i.fragments.length>0&&ns(s,i),!this.canLoad||!i.live)return;let a,o,l;if(i.canBlockReload&&i.endSN&&i.advanced){const t=this.hls.config.lowLatencyMode,r=i.lastPartSn,n=i.endSN,h=i.lastPartIndex,d=r===n;-1!==h?(o=d?n+1:r,l=d?t?0:h:h+1):o=n+1;const c=i.age,u=c+i.ageHeader;let f=Math.min(u-i.partTarget,1.5*i.targetduration);if(f>0){if(s&&f>s.tuneInGoal)this.warn(`CDN Tune-in goal increased from: ${s.tuneInGoal} to: ${f} with playlist age: ${i.age}`),f=0;else{const t=Math.floor(f/i.targetduration);if(o+=t,void 0!==l){l+=Math.round(f%i.targetduration/i.partTarget)}this.log(`CDN Tune-in age: ${i.ageHeader}s last advanced ${c.toFixed(2)}s goal: ${f} skip sn ${t} to part ${l}`)}i.tuneInGoal=f}if(a=this.getDeliveryDirectives(i,e.deliveryDirectives,o,l),t||!d)return void this.loadPlaylist(a)}else(i.canBlockReload||i.canSkipUntil)&&(a=this.getDeliveryDirectives(i,e.deliveryDirectives,o,l));const h=this.hls.mainForwardBufferInfo,d=h?h.end-h.len:0,c=function(t,e=1/0){let s=1e3*t.targetduration;if(t.updated){const i=t.fragments,r=4;if(i.length&&s*r>e){const t=1e3*i[i.length-1].duration;tthis.requestScheduled+c&&(this.requestScheduled=r.loading.start),void 0!==o&&i.canBlockReload?this.requestScheduled=r.loading.first+c-(1e3*i.partTarget||1e3):-1===this.requestScheduled||this.requestScheduled+cthis.loadPlaylist(a)),u)}else this.clearTimer()}getDeliveryDirectives(t,e,s,i){let r=Ze(t);return null!=e&&e.skip&&t.deltaUpdateFailed&&(s=e.msn,i=e.part,r=ze),new ts(s,i,r)}checkRetry(t){const e=t.details,s=cs(t),i=t.errorAction,{action:r,retryCount:n=0,retryConfig:a}=i||{},o=!!i&&!!a&&(r===As||!i.resolved&&r===Ss);if(o){var l;if(this.requestScheduled=-1,n>=a.maxNumRetry)return!1;if(s&&null!=(l=t.context)&&l.deliveryDirectives)this.warn(`Retrying playlist loading ${n+1}/${a.maxNumRetry} after "${e}" without delivery-directives`),this.loadPlaylist();else{const t=fs(a,n);this.timer=self.setTimeout((()=>this.loadPlaylist()),t),this.warn(`Retrying playlist loading ${n+1}/${a.maxNumRetry} after "${e}" in ${t}ms`)}t.levelRetry=!0,i.resolved=!0}return o}}class Ds{constructor(t,e=0,s=0){this.halfLife=void 0,this.alpha_=void 0,this.estimate_=void 0,this.totalWeight_=void 0,this.halfLife=t,this.alpha_=t?Math.exp(Math.log(.5)/t):0,this.estimate_=e,this.totalWeight_=s}sample(t,e){const s=Math.pow(this.alpha_,t);this.estimate_=e*(1-s)+s*this.estimate_,this.totalWeight_+=t}getTotalWeight(){return this.totalWeight_}getEstimate(){if(this.alpha_){const t=1-Math.pow(this.alpha_,this.totalWeight_);if(t)return this.estimate_/t}return this.estimate_}}class Is{constructor(t,e,s,i=100){this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultTTFB_=void 0,this.ttfb_=void 0,this.defaultEstimate_=s,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new Ds(t),this.fast_=new Ds(e),this.defaultTTFB_=i,this.ttfb_=new Ds(t)}update(t,e){const{slow_:s,fast_:i,ttfb_:r}=this;s.halfLife!==t&&(this.slow_=new Ds(t,s.getEstimate(),s.getTotalWeight())),i.halfLife!==e&&(this.fast_=new Ds(e,i.getEstimate(),i.getTotalWeight())),r.halfLife!==t&&(this.ttfb_=new Ds(t,r.getEstimate(),r.getTotalWeight()))}sample(t,e){const s=(t=Math.max(t,this.minDelayMs_))/1e3,i=8*e/s;this.fast_.sample(s,i),this.slow_.sample(s,i)}sampleTTFB(t){const e=t/1e3,s=Math.sqrt(2)*Math.exp(-Math.pow(e,2)/2);this.ttfb_.sample(s,Math.max(t,5))}canEstimate(){return this.fast_.getTotalWeight()>=this.minWeight_}getEstimate(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_}getEstimateTTFB(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_}destroy(){}}const _s={supported:!0,configurations:[],decodingInfoResults:[{supported:!0,powerEfficient:!0,smooth:!0}]},Cs={};function xs(t,e,s,i,r,n){const a=t.audioCodec?t.audioGroups:null,o=null==n?void 0:n.audioCodec,l=null==n?void 0:n.channels,h=l?parseInt(l):o?1/0:2;let d=null;if(null!=a&&a.length)try{d=1===a.length&&a[0]?e.groups[a[0]].channels:a.reduce(((t,s)=>{if(s){const i=e.groups[s];if(!i)throw new Error(`Audio track group ${s} not found`);Object.keys(i.channels).forEach((e=>{t[e]=(t[e]||0)+i.channels[e]}))}return t}),{2:0})}catch(t){return!0}return void 0!==t.videoCodec&&(t.width>1920&&t.height>1088||t.height>1920&&t.width>1088||t.frameRate>Math.max(i,30)||"SDR"!==t.videoRange&&t.videoRange!==s||t.bitrate>Math.max(r,8e6))||!!d&&f(h)&&Object.keys(d).some((t=>parseInt(t)>h))}function Ps(t,e,s){const i=t.videoCodec,r=t.audioCodec;if(!i||!r||!s)return Promise.resolve(_s);const n={width:t.width,height:t.height,bitrate:Math.ceil(Math.max(.9*t.bitrate,t.averageBitrate)),framerate:t.frameRate||30},a=t.videoRange;"SDR"!==a&&(n.transferFunction=a.toLowerCase());const o=i.split(",").map((t=>({type:"media-source",video:h(h({},n),{},{contentType:re(t,"video")})})));return r&&t.audioGroups&&t.audioGroups.forEach((t=>{var s;t&&(null==(s=e.groups[t])||s.tracks.forEach((e=>{if(e.groupId===t){const t=e.channels||"",s=parseFloat(t);f(s)&&s>2&&o.push.apply(o,r.split(",").map((t=>({type:"media-source",audio:{contentType:re(t,"audio"),channels:""+s}}))))}})))})),Promise.all(o.map((t=>{const e=function(t){const{audio:e,video:s}=t,i=s||e;if(i){const t=i.contentType.split('"')[1];if(s)return`r${s.height}x${s.width}f${Math.ceil(s.framerate)}${s.transferFunction||"sd"}_${t}_${Math.ceil(s.bitrate/1e5)}`;if(e)return`c${e.channels}${e.spatialRendering?"s":"n"}_${t}`}return""}(t);return Cs[e]||(Cs[e]=s.decodingInfo(t))}))).then((t=>({supported:!t.some((t=>!t.supported)),configurations:o,decodingInfoResults:t}))).catch((t=>({supported:!1,configurations:o,decodingInfoResults:[],error:t})))}function Ms(t,e){let s=!1,i=[];return t&&(s="SDR"!==t,i=[t]),e&&(i=e.allowedVideoRanges||Xe.slice(0),s=void 0!==e.preferHDR?e.preferHDR:function(){if("function"==typeof matchMedia){const t=matchMedia("(dynamic-range: high)"),e=matchMedia("bad query");if(t.media!==e.media)return!0===t.matches}return!1}(),i=s?i.filter((t=>"SDR"!==t)):["SDR"]),{preferHDR:s,allowedVideoRanges:i}}function Fs(t,e){A.log(`[abr] start candidates with "${t}" ignored because ${e}`)}function Os(t,e,s){if("attrs"in t){const s=e.indexOf(t);if(-1!==s)return s}for(let i=0;i-1===i.indexOf(t)))}(o,e.characteristics))&&(void 0===s||s(t,e))}function Us(t,e){const{audioCodec:s,channels:i}=t;return!(void 0!==s&&(e.audioCodec||"").substring(0,4)!==s.substring(0,4)||void 0!==i&&i!==(e.channels||"2"))}function Bs(t,e,s){for(let i=e;i;i--)if(s(t[i]))return i;for(let i=e+1;i1&&this.tickImmediate(),this._tickCallCount=0)}tickImmediate(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)}doTick(){}}var Gs="NOT_LOADED",Ks="APPENDING",Hs="PARTIAL",Vs="OK";class Ys{constructor(t){this.activePartLists=Object.create(null),this.endListFragments=Object.create(null),this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hasGaps=!1,this.hls=t,this._registerListeners()}_registerListeners(){const{hls:t}=this;t.on(p.BUFFER_APPENDED,this.onBufferAppended,this),t.on(p.FRAG_BUFFERED,this.onFragBuffered,this),t.on(p.FRAG_LOADED,this.onFragLoaded,this)}_unregisterListeners(){const{hls:t}=this;t.off(p.BUFFER_APPENDED,this.onBufferAppended,this),t.off(p.FRAG_BUFFERED,this.onFragBuffered,this),t.off(p.FRAG_LOADED,this.onFragLoaded,this)}destroy(){this._unregisterListeners(),this.fragments=this.activePartLists=this.endListFragments=this.timeRanges=null}getAppendedFrag(t,e){const s=this.activePartLists[e];if(s)for(let e=s.length;e--;){const i=s[e];if(!i)break;const r=i.end;if(i.start<=t&&null!==r&&t<=r)return i}return this.getBufferedFrag(t,e)}getBufferedFrag(t,e){const{fragments:s}=this,i=Object.keys(s);for(let r=i.length;r--;){const n=s[i[r]];if((null==n?void 0:n.body.type)===e&&n.buffered){const e=n.body;if(e.start<=t&&t<=e.end)return e}}return null}detectEvictedFragments(t,e,s,i){this.timeRanges&&(this.timeRanges[t]=e);const r=(null==i?void 0:i.fragment.sn)||-1;Object.keys(this.fragments).forEach((i=>{const n=this.fragments[i];if(!n)return;if(r>=n.body.sn)return;if(!n.buffered&&!n.loaded)return void(n.body.type===s&&this.removeFragment(n.body));const a=n.range[t];a&&a.time.some((t=>{const s=!this.isTimeBuffered(t.startPTS,t.endPTS,e);return s&&this.removeFragment(n.body),s}))}))}detectPartialFragments(t){const e=this.timeRanges,{frag:s,part:i}=t;if(!e||"initSegment"===s.sn)return;const r=js(s),n=this.fragments[r];if(!n||n.buffered&&s.gap)return;const a=!s.relurl;if(Object.keys(e).forEach((t=>{const r=s.elementaryStreams[t];if(!r)return;const o=e[t],l=a||!0===r.partial;n.range[t]=this.getBufferedTimes(s,i,l,o)})),n.loaded=null,Object.keys(n.range).length){n.buffered=!0;(n.body.endList=s.endList||n.body.endList)&&(this.endListFragments[n.body.type]=n),Ws(n)||this.removeParts(s.sn-1,s.type)}else this.removeFragment(n.body)}removeParts(t,e){const s=this.activePartLists[e];s&&(this.activePartLists[e]=s.filter((e=>e.fragment.sn>=t)))}fragBuffered(t,e){const s=js(t);let i=this.fragments[s];!i&&e&&(i=this.fragments[s]={body:t,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},t.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,i.buffered=!0)}getBufferedTimes(t,e,s,i){const r={time:[],partial:s},n=t.start,a=t.end,o=t.minEndPTS||a,l=t.maxStartPTS||n;for(let t=0;t=e&&o<=s){r.time.push({startPTS:Math.max(n,i.start(t)),endPTS:Math.min(a,i.end(t))});break}if(ne){const e=Math.max(n,i.start(t)),s=Math.min(a,i.end(t));s>e&&(r.partial=!0,r.time.push({startPTS:e,endPTS:s}))}else if(a<=e)break}return r}getPartialFragment(t){let e,s,i,r=null,n=0;const{bufferPadding:a,fragments:o}=this;return Object.keys(o).forEach((l=>{const h=o[l];h&&Ws(h)&&(s=h.body.start-a,i=h.body.end+a,t>=s&&t<=i&&(e=Math.min(t-s,i-t),n<=e&&(r=h.body,n=e)))})),r}isEndListAppended(t){const e=this.endListFragments[t];return void 0!==e&&(e.buffered||Ws(e))}getState(t){const e=js(t),s=this.fragments[e];return s?s.buffered?Ws(s)?Hs:Vs:Ks:Gs}isTimeBuffered(t,e,s){let i,r;for(let n=0;n=i&&e<=r)return!0;if(e<=i)return!1}return!1}onFragLoaded(t,e){const{frag:s,part:i}=e;if("initSegment"===s.sn||s.bitrateTest)return;const r=i?null:e,n=js(s);this.fragments[n]={body:s,appendedPTS:null,loaded:r,buffered:!1,range:Object.create(null)}}onBufferAppended(t,e){const{frag:s,part:i,timeRanges:r}=e;if("initSegment"===s.sn)return;const n=s.type;if(i){let t=this.activePartLists[n];t||(this.activePartLists[n]=t=[]),t.push(i)}this.timeRanges=r,Object.keys(r).forEach((t=>{const e=r[t];this.detectEvictedFragments(t,e,n,i)}))}onFragBuffered(t,e){this.detectPartialFragments(e)}hasFragment(t){const e=js(t);return!!this.fragments[e]}hasParts(t){var e;return!(null==(e=this.activePartLists[t])||!e.length)}removeFragmentsInRange(t,e,s,i,r){i&&!this.hasGaps||Object.keys(this.fragments).forEach((n=>{const a=this.fragments[n];if(!a)return;const o=a.body;o.type!==s||i&&!o.gap||o.startt&&(a.buffered||r)&&this.removeFragment(o)}))}removeFragment(t){const e=js(t);t.stats.loaded=0,t.clearElementaryStreamInfo();const s=this.activePartLists[t.type];if(s){const e=t.sn;this.activePartLists[t.type]=s.filter((t=>t.fragment.sn!==e))}delete this.fragments[e],t.endList&&delete this.endListFragments[t.type]}removeAllFragments(){this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1}}function Ws(t){var e,s,i;return t.buffered&&(t.body.gap||(null==(e=t.range.video)?void 0:e.partial)||(null==(s=t.range.audio)?void 0:s.partial)||(null==(i=t.range.audiovideo)?void 0:i.partial))}function js(t){return`${t.type}_${t.level}_${t.sn}`}const qs={length:0,start:()=>0,end:()=>0};class Xs{static isBuffered(t,e){try{if(t){const s=Xs.getBuffered(t);for(let t=0;t=s.start(t)&&e<=s.end(t))return!0}}catch(t){}return!1}static bufferInfo(t,e,s){try{if(t){const i=Xs.getBuffered(t),r=[];let n;for(n=0;nn&&(i[r-1].end=t[e].end):i.push(t[e])}else i.push(t[e])}else i=t;let r,n=0,a=e,o=e;for(let t=0;t=l&&es.startCC||t&&t.cc{if(this.loader&&this.loader.destroy(),t.gap){if(t.tagList.some((t=>"GAP"===t[0])))return void o(ni(t));t.gap=!1}const l=this.loader=t.loader=r?new r(i):new n(i),d=ri(t),c=gs(i.fragLoadPolicy.default),u={loadPolicy:c,timeout:c.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0,highWaterMark:"initSegment"===t.sn?1/0:si};t.stats=l.stats,l.load(d,u,{onSuccess:(e,s,i,r)=>{this.resetLoader(t,l);let n=e.data;i.resetIV&&t.decryptdata&&(t.decryptdata.iv=new Uint8Array(n.slice(0,16)),n=n.slice(16)),a({frag:t,part:null,payload:n,networkDetails:r})},onError:(e,i,r,n)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.FRAG_LOAD_ERROR,fatal:!1,frag:t,response:h({url:s,data:void 0},e),error:new Error(`HTTP Error ${e.code} ${e.text}`),networkDetails:r,stats:n}))},onAbort:(e,s,i)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.INTERNAL_ABORTED,fatal:!1,frag:t,error:new Error("Aborted"),networkDetails:i,stats:e}))},onTimeout:(e,s,i)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.FRAG_LOAD_TIMEOUT,fatal:!1,frag:t,error:new Error(`Timeout after ${u.timeout}ms`),networkDetails:i,stats:e}))},onProgress:(s,i,r,n)=>{e&&e({frag:t,part:null,payload:r,networkDetails:n})}})}))}loadPart(t,e,s){this.abort();const i=this.config,r=i.fLoader,n=i.loader;return new Promise(((a,o)=>{if(this.loader&&this.loader.destroy(),t.gap||e.gap)return void o(ni(t,e));const l=this.loader=t.loader=r?new r(i):new n(i),d=ri(t,e),c=gs(i.fragLoadPolicy.default),u={loadPolicy:c,timeout:c.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0,highWaterMark:si};e.stats=l.stats,l.load(d,u,{onSuccess:(i,r,n,o)=>{this.resetLoader(t,l),this.updateStatsFromPart(t,e);const h={frag:t,part:e,payload:i.data,networkDetails:o};s(h),a(h)},onError:(s,i,r,n)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.FRAG_LOAD_ERROR,fatal:!1,frag:t,part:e,response:h({url:d.url,data:void 0},s),error:new Error(`HTTP Error ${s.code} ${s.text}`),networkDetails:r,stats:n}))},onAbort:(s,i,r)=>{t.stats.aborted=e.stats.aborted,this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.INTERNAL_ABORTED,fatal:!1,frag:t,part:e,error:new Error("Aborted"),networkDetails:r,stats:s}))},onTimeout:(s,i,r)=>{this.resetLoader(t,l),o(new ai({type:v.NETWORK_ERROR,details:y.FRAG_LOAD_TIMEOUT,fatal:!1,frag:t,part:e,error:new Error(`Timeout after ${u.timeout}ms`),networkDetails:r,stats:s}))}})}))}updateStatsFromPart(t,e){const s=t.stats,i=e.stats,r=i.total;if(s.loaded+=i.loaded,r){const i=Math.round(t.duration/e.duration),n=Math.min(Math.round(s.loaded/r),i),a=(i-n)*Math.round(s.loaded/n);s.total=s.loaded+a}else s.total=Math.max(s.loaded,s.total);const n=s.loading,a=i.loading;n.start?n.first+=a.first-a.start:(n.start=a.start,n.first=a.first),n.end=a.end}resetLoader(t,e){t.loader=null,this.loader===e&&(self.clearTimeout(this.partLoadTimeout),this.loader=null),e.destroy()}}function ri(t,e=null){const s=e||t,i={frag:t,part:e,responseType:"arraybuffer",url:s.url,headers:{},rangeStart:0,rangeEnd:0},r=s.byteRangeStartOffset,n=s.byteRangeEndOffset;if(f(r)&&f(n)){var a;let e=r,s=n;if("initSegment"===t.sn&&"AES-128"===(null==(a=t.decryptdata)?void 0:a.method)){const t=n-r;t%16&&(s=n+(16-t%16)),0!==r&&(i.resetIV=!0,e=r-16)}i.rangeStart=e,i.rangeEnd=s}return i}function ni(t,e){const s=new Error(`GAP ${t.gap?"tag":"attribute"} found`),i={type:v.MEDIA_ERROR,details:y.FRAG_GAP,fatal:!1,frag:t,error:s,networkDetails:null};return e&&(i.part=e),(e||t).stats.aborted=!0,new ai(i)}class ai extends Error{constructor(t){super(t.error.message),this.data=void 0,this.data=t}}class oi{constructor(t,e){this.subtle=void 0,this.aesIV=void 0,this.subtle=t,this.aesIV=e}decrypt(t,e){return this.subtle.decrypt({name:"AES-CBC",iv:this.aesIV},e,t)}}class li{constructor(t,e){this.subtle=void 0,this.key=void 0,this.subtle=t,this.key=e}expandKey(){return this.subtle.importKey("raw",this.key,{name:"AES-CBC"},!1,["encrypt","decrypt"])}}class hi{constructor(){this.rcon=[0,1,2,4,8,16,32,64,128,27,54],this.subMix=[new Uint32Array(256),new Uint32Array(256),new Uint32Array(256),new Uint32Array(256)],this.invSubMix=[new Uint32Array(256),new Uint32Array(256),new Uint32Array(256),new Uint32Array(256)],this.sBox=new Uint32Array(256),this.invSBox=new Uint32Array(256),this.key=new Uint32Array(0),this.ksRows=0,this.keySize=0,this.keySchedule=void 0,this.invKeySchedule=void 0,this.initTable()}uint8ArrayToUint32Array_(t){const e=new DataView(t),s=new Uint32Array(4);for(let t=0;t<4;t++)s[t]=e.getUint32(4*t);return s}initTable(){const t=this.sBox,e=this.invSBox,s=this.subMix,i=s[0],r=s[1],n=s[2],a=s[3],o=this.invSubMix,l=o[0],h=o[1],d=o[2],c=o[3],u=new Uint32Array(256);let f=0,g=0,m=0;for(m=0;m<256;m++)u[m]=m<128?m<<1:m<<1^283;for(m=0;m<256;m++){let s=g^g<<1^g<<2^g<<3^g<<4;s=s>>>8^255&s^99,t[f]=s,e[s]=f;const o=u[f],m=u[o],p=u[m];let v=257*u[s]^16843008*s;i[f]=v<<24|v>>>8,r[f]=v<<16|v>>>16,n[f]=v<<8|v>>>24,a[f]=v,v=16843009*p^65537*m^257*o^16843008*f,l[s]=v<<24|v>>>8,h[s]=v<<16|v>>>16,d[s]=v<<8|v>>>24,c[s]=v,f?(f=o^u[u[u[p^o]]],g^=u[u[g]]):f=g=1}}expandKey(t){const e=this.uint8ArrayToUint32Array_(t);let s=!0,i=0;for(;i{if(!this.subtle)return Promise.reject(new Error("web crypto not initialized"));this.logOnce("WebCrypto AES decrypt");return new oi(this.subtle,new Uint8Array(s)).decrypt(t.buffer,e)})).catch((i=>(A.warn(`[decrypter]: WebCrypto Error, disable WebCrypto API, ${i.name}: ${i.message}`),this.onWebCryptoError(t,e,s))))}onWebCryptoError(t,e,s){this.useSoftware=!0,this.logEnabled=!0,this.softwareDecrypt(t,e,s);const i=this.flush();if(i)return i.buffer;throw new Error("WebCrypto and softwareDecrypt: failed to decrypt data")}getValidChunk(t){let e=t;const s=t.length-t.length%16;return s!==t.length&&(e=st(t,0,s),this.remainderData=st(t,s)),e}logOnce(t){this.logEnabled&&(A.log(`[decrypter]: ${t}`),this.logEnabled=!1)}}const ci=function(t){let e="";const s=t.length;for(let i=0;ia.end){const t=n>r;(n{if(this.fragContextChanged(t))return this.warn(`Fragment ${t.sn}${e.part?" p: "+e.part.index:""} of level ${t.level} was dropped during download.`),void this.fragmentTracker.removeFragment(t);t.stats.chunkCount++,this._handleFragmentLoadProgress(e)})).then((e=>{if(!e)return;const s=this.state;this.fragContextChanged(t)?(s===mi||!this.fragCurrent&&s===yi)&&(this.fragmentTracker.removeFragment(t),this.state=fi):("payload"in e&&(this.log(`Loaded fragment ${t.sn} of level ${t.level}`),this.hls.trigger(p.FRAG_LOADED,e)),this._handleFragmentLoadComplete(e))})).catch((e=>{this.state!==ui&&this.state!==Si&&(this.warn(`Frag error: ${(null==e?void 0:e.message)||e}`),this.resetFragmentLoading(t))}))}clearTrackerIfNeeded(t){var e;const{fragmentTracker:s}=this;if(s.getState(t)===Ks){const e=t.type,i=this.getFwdBufferInfo(this.mediaBuffer,e),r=Math.max(t.duration,i?i.len:this.config.maxBufferLength),n=this.backtrackFragment;(1===(n?t.sn-n.sn:0)||this.reduceMaxBufferLength(r,t.duration))&&s.removeFragment(t)}else 0===(null==(e=this.mediaBuffer)?void 0:e.buffered.length)?s.removeAllFragments():s.hasParts(t.type)&&(s.detectPartialFragments({frag:t,part:null,stats:t.stats,id:t.type}),s.getState(t)===Hs&&s.removeFragment(t))}checkLiveUpdate(t){if(t.updated&&!t.live){const e=t.fragments[t.fragments.length-1];this.fragmentTracker.detectPartialFragments({frag:e,part:null,stats:e.stats,id:e.type})}t.fragments[0]||(t.deltaUpdateFailed=!0)}flushMainBuffer(t,e,s=null){if(!(t-e))return;const i={startOffset:t,endOffset:e,type:s};this.hls.trigger(p.BUFFER_FLUSHING,i)}_loadInitSegment(t,e){this._doFragLoad(t,e).then((e=>{if(!e||this.fragContextChanged(t)||!this.levels)throw new Error("init load aborted");return e})).then((e=>{const{hls:s}=this,{payload:i}=e,r=t.decryptdata;if(i&&i.byteLength>0&&null!=r&&r.key&&r.iv&&"AES-128"===r.method){const n=self.performance.now();return this.decrypter.decrypt(new Uint8Array(i),r.key.buffer,r.iv.buffer).catch((e=>{throw s.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:t}),e})).then((i=>{const r=self.performance.now();return s.trigger(p.FRAG_DECRYPTED,{frag:t,payload:i,stats:{tstart:n,tdecrypt:r}}),e.payload=i,this.completeInitSegmentLoad(e)}))}return this.completeInitSegmentLoad(e)})).catch((e=>{this.state!==ui&&this.state!==Si&&(this.warn(e),this.resetFragmentLoading(t))}))}completeInitSegmentLoad(t){const{levels:e}=this;if(!e)throw new Error("init load aborted, missing levels");const s=t.frag.stats;this.state=fi,t.frag.data=new Uint8Array(t.payload),s.parsing.start=s.buffering.start=self.performance.now(),s.parsing.end=s.buffering.end=self.performance.now(),this.tick()}fragContextChanged(t){const{fragCurrent:e}=this;return!t||!e||t.sn!==e.sn||t.level!==e.level}fragBufferedComplete(t,e){var s,i,r,n;const a=this.mediaBuffer?this.mediaBuffer:this.media;if(this.log(`Buffered ${t.type} sn: ${t.sn}${e?" part: "+e.index:""} of ${this.playlistType===De?"level":"track"} ${t.level} (frag:[${(null!=(s=t.startPTS)?s:NaN).toFixed(3)}-${(null!=(i=t.endPTS)?i:NaN).toFixed(3)}] > buffer:${a?ci(Xs.getBuffered(a)):"(detached)"})`),"initSegment"!==t.sn){var o;if(t.type!==_e){const e=t.elementaryStreams;if(!Object.keys(e).some((t=>!!e[t])))return void(this.state=fi)}const e=null==(o=this.levels)?void 0:o[t.level];null!=e&&e.fragmentError&&(this.log(`Resetting level fragment error count of ${e.fragmentError} on frag buffered`),e.fragmentError=0)}this.state=fi,a&&(!this.loadedmetadata&&t.type==De&&a.buffered.length&&(null==(r=this.fragCurrent)?void 0:r.sn)===(null==(n=this.fragPrevious)?void 0:n.sn)&&(this.loadedmetadata=!0,this.seekToStartPos()),this.tick())}seekToStartPos(){}_handleFragmentLoadComplete(t){const{transmuxer:e}=this;if(!e)return;const{frag:s,part:i,partsLoaded:r}=t,n=!r||0===r.length||r.some((t=>!t)),a=new zs(s.level,s.sn,s.stats.chunkCount+1,0,i?i.index:-1,!n);e.flush(a)}_handleFragmentLoadProgress(t){}_doFragLoad(t,e,s=null,i){var r;const n=null==e?void 0:e.details;if(!this.levels||!n)throw new Error(`frag load aborted, missing level${n?"":" detail"}s`);let a=null;if(!t.encrypted||null!=(r=t.decryptdata)&&r.key?!t.encrypted&&n.encryptedFragments.length&&this.keyLoader.loadClear(t,n.encryptedFragments):(this.log(`Loading key for ${t.sn} of [${n.startSN}-${n.endSN}], ${"[stream-controller]"===this.logPrefix?"level":"track"} ${t.level}`),this.state=gi,this.fragCurrent=t,a=this.keyLoader.load(t).then((t=>{if(!this.fragContextChanged(t.frag))return this.hls.trigger(p.KEY_LOADED,t),this.state===gi&&(this.state=fi),t})),this.hls.trigger(p.KEY_LOADING,{frag:t}),null===this.fragCurrent&&(a=Promise.reject(new Error("frag load aborted, context changed in KEY_LOADING")))),s=Math.max(t.start,s||0),this.config.lowLatencyMode&&"initSegment"!==t.sn){const r=n.partList;if(r&&i){s>t.end&&n.fragmentHint&&(t=n.fragmentHint);const o=this.getNextPart(r,t,s);if(o>-1){const l=r[o];let h;return this.log(`Loading part sn: ${t.sn} p: ${l.index} cc: ${t.cc} of playlist [${n.startSN}-${n.endSN}] parts [0-${o}-${r.length-1}] ${"[stream-controller]"===this.logPrefix?"level":"track"}: ${t.level}, target: ${parseFloat(s.toFixed(3))}`),this.nextLoadPosition=l.start+l.duration,this.state=mi,h=a?a.then((s=>!s||this.fragContextChanged(s.frag)?null:this.doFragPartsLoad(t,l,e,i))).catch((t=>this.handleFragLoadError(t))):this.doFragPartsLoad(t,l,e,i).catch((t=>this.handleFragLoadError(t))),this.hls.trigger(p.FRAG_LOADING,{frag:t,part:l,targetBufferTime:s}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):h}if(!t.url||this.loadedEndOfParts(r,s))return Promise.resolve(null)}}this.log(`Loading fragment ${t.sn} cc: ${t.cc} ${n?"of ["+n.startSN+"-"+n.endSN+"] ":""}${"[stream-controller]"===this.logPrefix?"level":"track"}: ${t.level}, target: ${parseFloat(s.toFixed(3))}`),f(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=mi;const o=this.config.progressive;let l;return l=o&&a?a.then((e=>!e||this.fragContextChanged(null==e?void 0:e.frag)?null:this.fragmentLoader.load(t,i))).catch((t=>this.handleFragLoadError(t))):Promise.all([this.fragmentLoader.load(t,o?i:void 0),a]).then((([t])=>(!o&&t&&i&&i(t),t))).catch((t=>this.handleFragLoadError(t))),this.hls.trigger(p.FRAG_LOADING,{frag:t,targetBufferTime:s}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):l}doFragPartsLoad(t,e,s,i){return new Promise(((r,n)=>{var a;const o=[],l=null==(a=s.details)?void 0:a.partList,h=e=>{this.fragmentLoader.loadPart(t,e,i).then((i=>{o[e.index]=i;const n=i.part;this.hls.trigger(p.FRAG_LOADED,i);const a=ls(s,t.sn,e.index+1)||hs(l,t.sn,e.index+1);if(!a)return r({frag:t,part:n,partsLoaded:o});h(a)})).catch(n)};h(e)}))}handleFragLoadError(t){if("data"in t){const e=t.data;t.data&&e.details===y.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(p.ERROR,e)}else this.hls.trigger(p.ERROR,{type:v.OTHER_ERROR,details:y.INTERNAL_EXCEPTION,err:t,error:t,fatal:!0});return null}_handleTransmuxerFlush(t){const e=this.getCurrentContext(t);if(!e||this.state!==yi)return void(this.fragCurrent||this.state===ui||this.state===Si||(this.state=fi));const{frag:s,part:i,level:r}=e,n=self.performance.now();s.stats.parsing.end=n,i&&(i.stats.parsing.end=n),this.updateLevelTiming(s,i,r,t.partial)}getCurrentContext(t){const{levels:e,fragCurrent:s}=this,{level:i,sn:r,part:n}=t;if(null==e||!e[i])return this.warn(`Levels object was unset while buffering fragment ${r} of level ${i}. The current chunk will not be buffered.`),null;const a=e[i],o=n>-1?ls(a,r,n):null,l=o?o.fragment:function(t,e,s){if(null==t||!t.details)return null;const i=t.details;let r=i.fragments[e-i.startSN];return r||(r=i.fragmentHint,r&&r.sn===e?r:en&&this.flushMainBuffer(a,t.start)}getFwdBufferInfo(t,e){const s=this.getLoadPosition();return f(s)?this.getFwdBufferInfoAtPos(t,s,e):null}getFwdBufferInfoAtPos(t,e,s){const{config:{maxBufferHole:i}}=this,r=Xs.bufferInfo(t,e,i);if(0===r.len&&void 0!==r.nextStart){const n=this.fragmentTracker.getBufferedFrag(e,s);if(n&&r.nextStart=i&&(s.maxMaxBufferLength=r,this.warn(`Reduce max buffer length to ${r}s`),!0)}getAppendedFrag(t,e=De){const s=this.fragmentTracker.getAppendedFrag(t,De);return s&&"fragment"in s?s.fragment:s}getNextFragment(t,e){const s=e.fragments,i=s.length;if(!i)return null;const{config:r}=this,n=s[0].start;let a;if(e.live){const o=r.initialLiveManifestSize;if(ie}getNextFragmentLoopLoading(t,e,s,i,r){const n=t.gap,a=this.getNextFragment(this.nextLoadPosition,e);if(null===a)return a;if(t=a,n&&t&&!t.gap&&s.nextStart){const e=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,s.nextStart,i);if(null!==e&&s.len+e.len>=r)return this.log(`buffer full after gaps in "${i}" playlist starting at sn: ${t.sn}`),null}return t}mapToInitFragWhenRequired(t){return null==t||!t.initSegment||null!=t&&t.initSegment.data||this.bitrateTest?t:t.initSegment}getNextPart(t,e,s){let i=-1,r=!1,n=!0;for(let a=0,o=t.length;a-1&&ss.start&&s.loaded}getInitialLiveFragment(t,e){const s=this.fragPrevious;let i=null;if(s){if(t.hasProgramDateTime&&(this.log(`Live playlist, switching playlist, load frag with same PDT: ${s.programDateTime}`),i=function(t,e,s){if(null===e||!Array.isArray(t)||!t.length||!f(e))return null;if(e<(t[0].programDateTime||0))return null;if(e>=(t[t.length-1].endProgramDateTime||0))return null;s=s||0;for(let i=0;i=t.startSN&&r<=t.endSN){const n=e[r-t.startSN];s.cc===n.cc&&(i=n,this.log(`Live playlist, switching playlist, load frag with next SN: ${i.sn}`))}i||(i=function(t,e){return ps(t,(t=>t.cce?-1:0))}(e,s.cc),i&&this.log(`Live playlist, switching playlist, load frag with same CC: ${i.sn}`))}}else{const e=this.hls.liveSyncPosition;null!==e&&(i=this.getFragmentAtPosition(e,this.bitrateTest?t.fragmentEnd:t.edge,t))}return i}getFragmentAtPosition(t,e,s){const{config:i}=this;let{fragPrevious:r}=this,{fragments:n,endSN:a}=s;const{fragmentHint:o}=s,{maxFragLookUpTolerance:l}=i,h=s.partList,d=!!(i.lowLatencyMode&&null!=h&&h.length&&o);let c;if(d&&o&&!this.bitrateTest&&(n=n.concat(o),a=o.sn),te-l?0:l)}else c=n[n.length-1];if(c){const t=c.sn-s.startSN,e=this.fragmentTracker.getState(c);if((e===Vs||e===Hs&&c.gap)&&(r=c),r&&c.sn===r.sn&&(!d||h[0].fragment.sn>c.sn)){if(r&&c.level===r.level){const e=n[t+1];c=c.sn=n-e.maxFragLookUpTolerance&&r<=a;if(null!==i&&s.duration>i&&(r${t.startSN} prev-sn: ${r?r.sn:"na"} fragments: ${i}`),n}return r}waitForCdnTuneIn(t){return t.live&&t.canBlockReload&&t.partTarget&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)}setStartPosition(t,e){let s=this.startPosition;if(s ${null==(i=this.fragCurrent)?void 0:i.url}`);const r=e.details===y.FRAG_GAP;r&&this.fragmentTracker.fragBuffered(s,!0);const n=e.errorAction,{action:a,retryCount:o=0,retryConfig:l}=n||{};if(n&&a===As&&l){this.resetStartWhenNotLoaded(this.levelLastLoaded);const i=fs(l,o);this.warn(`Fragment ${s.sn} of ${t} ${s.level} errored with ${e.details}, retrying loading ${o+1}/${l.maxNumRetry} in ${i}ms`),n.resolved=!0,this.retryDate=self.performance.now()+i,this.state=pi}else if(l&&n){if(this.resetFragmentErrors(t),!(o.5;r&&this.reduceMaxBufferLength(i.len,(null==e?void 0:e.duration)||10);const n=!r;return n&&this.warn(`Buffer full error while media.currentTime is not buffered, flush ${s} buffer`),e&&(this.fragmentTracker.removeFragment(e),this.nextLoadPosition=e.start),this.resetLoadingState(),n}return!1}resetFragmentErrors(t){t===Ie&&(this.fragCurrent=null),this.loadedmetadata||(this.startFragRequested=!1),this.state!==ui&&(this.state=fi)}afterBufferFlushed(t,e,s){if(!t)return;const i=Xs.getBuffered(t);this.fragmentTracker.detectEvictedFragments(e,i,s),this.state===Ti&&this.resetLoadingState()}resetLoadingState(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state=fi}resetStartWhenNotLoaded(t){if(!this.loadedmetadata){this.startFragRequested=!1;const e=t?t.details:null;null!=e&&e.live?(this.startPosition=-1,this.setStartPosition(e,0),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}}resetWhenMissingContext(t){this.warn(`The loading context changed while buffering fragment ${t.sn} of level ${t.level}. This chunk will not be buffered.`),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState()}removeUnbufferedFrags(t=0){this.fragmentTracker.removeFragmentsInRange(t,1/0,this.playlistType,!1,!0)}updateLevelTiming(t,e,s,i){var r;const n=s.details;if(!n)return void this.warn("level.details undefined");if(!Object.keys(t.elementaryStreams).reduce(((e,r)=>{const a=t.elementaryStreams[r];if(a){const o=a.endPTS-a.startPTS;if(o<=0)return this.warn(`Could not parse fragment ${t.sn} ${r} duration reliably (${o})`),e||!1;const l=i?0:rs(n,t,a.startPTS,a.endPTS,a.startDTS,a.endDTS);return this.hls.trigger(p.LEVEL_PTS_UPDATED,{details:n,level:s,drift:l,type:r,frag:t,start:a.startPTS,end:a.endPTS}),!0}return e}),!1)&&null===(null==(r=this.transmuxer)?void 0:r.error)){const e=new Error(`Found no media in fragment ${t.sn} of level ${t.level} resetting transmuxer to fallback to playlist timing`);if(0===s.fragmentError&&(s.fragmentError++,t.gap=!0,this.fragmentTracker.removeFragment(t),this.fragmentTracker.fragBuffered(t,!0)),this.warn(e.message),this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,fatal:!1,error:e,frag:t,reason:`Found no media in msn ${t.sn} of level "${s.url}"`}),!this.hls)return;this.resetTransmuxer()}this.state=Ei,this.hls.trigger(p.FRAG_PARSED,{frag:t,part:e})}resetTransmuxer(){this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null)}recoverWorkerError(t){"demuxerWorker"===t.event&&(this.fragmentTracker.removeAllFragments(),this.resetTransmuxer(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState())}set state(t){const e=this._state;e!==t&&(this._state=t,this.log(`${e}->${t}`))}get state(){return this._state}}class bi{constructor(){this.chunks=[],this.dataLength=0}push(t){this.chunks.push(t),this.dataLength+=t.length}flush(){const{chunks:t,dataLength:e}=this;let s;return t.length?(s=1===t.length?t[0]:function(t,e){const s=new Uint8Array(e);let i=0;for(let e=0;e0&&a.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:Be,duration:Number.POSITIVE_INFINITY});r{if(f(t))return 90*t;return 9e4*e+(s?9e4*s.baseTime/s.timescale:0)};function Ii(t,e){return 255===t[e]&&240==(246&t[e+1])}function _i(t,e){return 1&t[e+1]?7:9}function Ci(t,e){return(3&t[e+3])<<11|t[e+4]<<3|(224&t[e+5])>>>5}function xi(t,e){return e+1=t.length)return!1;const i=Ci(t,e);if(i<=s)return!1;const r=e+i;return r===t.length||xi(t,r)}return!1}function Mi(t,e,s,i,r){if(!t.samplerate){const n=function(t,e,s,i){let r,n,a,o;const l=navigator.userAgent.toLowerCase(),h=i,d=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];r=1+((192&e[s+2])>>>6);const c=(60&e[s+2])>>>2;if(!(c>d.length-1))return a=(1&e[s+2])<<2,a|=(192&e[s+3])>>>6,A.log(`manifest codec:${i}, ADTS type:${r}, samplingIndex:${c}`),/firefox/i.test(l)?c>=6?(r=5,o=new Array(4),n=c-3):(r=2,o=new Array(2),n=c):-1!==l.indexOf("android")?(r=2,o=new Array(2),n=c):(r=5,o=new Array(4),i&&(-1!==i.indexOf("mp4a.40.29")||-1!==i.indexOf("mp4a.40.5"))||!i&&c>=6?n=c-3:((i&&-1!==i.indexOf("mp4a.40.2")&&(c>=6&&1===a||/vivaldi/i.test(l))||!i&&1===a)&&(r=2,o=new Array(2)),n=c)),o[0]=r<<3,o[0]|=(14&c)>>1,o[1]|=(1&c)<<7,o[1]|=a<<3,5===r&&(o[1]|=(14&n)>>1,o[2]=(1&n)<<7,o[2]|=8,o[3]=0),{config:o,samplerate:d[c],channelCount:a,codec:"mp4a.40."+r,manifestCodec:h};{const e=new Error(`invalid ADTS sampling index:${c}`);t.emit(p.ERROR,p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,fatal:!0,error:e,reason:e.message})}}(e,s,i,r);if(!n)return;t.config=n.config,t.samplerate=n.samplerate,t.channelCount=n.channelCount,t.codec=n.codec,t.manifestCodec=n.manifestCodec,A.log(`parsed codec:${t.codec}, rate:${n.samplerate}, channels:${n.channelCount}`)}}function Fi(t){return 9216e4/t}function Oi(t,e,s,i,r){const n=i+r*Fi(t.samplerate),a=function(t,e){const s=_i(t,e);if(e+s<=t.length){const i=Ci(t,e)-s;if(i>0)return{headerLength:s,frameLength:i}}}(e,s);let o;if(a){const{frameLength:i,headerLength:r}=a,l=r+i,h=Math.max(0,s+l-e.length);h?(o=new Uint8Array(l-r),o.set(e.subarray(s+r,e.length),0)):o=e.subarray(s+r,s+l);const d={unit:o,pts:n};return h||t.samples.push(d),{sample:d,length:l,missing:h}}const l=e.length-s;o=new Uint8Array(l),o.set(e.subarray(s,e.length),0);return{sample:{unit:o,pts:n},length:l,missing:-1}}let Ni=null;const Ui=[32,64,96,128,160,192,224,256,288,320,352,384,416,448,32,48,56,64,80,96,112,128,160,192,224,256,320,384,32,40,48,56,64,80,96,112,128,160,192,224,256,320,32,48,56,64,80,96,112,128,144,160,176,192,224,256,8,16,24,32,40,48,56,64,80,96,112,128,144,160],Bi=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],$i=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],Gi=[0,1,1,4];function Ki(t,e,s,i,r){if(s+24>e.length)return;const n=Hi(e,s);if(n&&s+n.frameLength<=e.length){const a=i+r*(9e4*n.samplesPerFrame/n.sampleRate),o={unit:e.subarray(s,s+n.frameLength),pts:a,dts:a};return t.config=[],t.channelCount=n.channelCount,t.samplerate=n.sampleRate,t.samples.push(o),{sample:o,length:n.frameLength,missing:0}}}function Hi(t,e){const s=t[e+1]>>3&3,i=t[e+1]>>1&3,r=t[e+2]>>4&15,n=t[e+2]>>2&3;if(1!==s&&0!==r&&15!==r&&3!==n){const a=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*Ui[14*(3===s?3-i:3===i?3:4)+r-1],h=Bi[3*(3===s?0:2===s?1:2)+n],d=3===o?1:2,c=$i[s][i],u=Gi[i],f=8*c*u,g=Math.floor(c*l/h+a)*u;if(null===Ni){const t=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Ni=t?parseInt(t[1]):0}return!!Ni&&Ni<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:h,channelCount:d,frameLength:g,samplesPerFrame:f}}}function Vi(t,e){return!(255!==t[e]||224&~t[e+1]||!(6&t[e+1]))}function Yi(t,e){return e+1{let s=0,i=5;e+=i;const r=new Uint32Array(1),n=new Uint32Array(1),a=new Uint8Array(1);for(;i>0;){a[0]=t[e];const o=Math.min(i,8),l=8-o;n[0]=4278190080>>>24+l<>l,s=s?s<e.length)return-1;if(11!==e[s]||119!==e[s+1])return-1;const n=e[s+4]>>6;if(n>=3)return-1;const a=[48e3,44100,32e3][n],o=63&e[s+4],l=2*[64,69,96,64,70,96,80,87,120,80,88,120,96,104,144,96,105,144,112,121,168,112,122,168,128,139,192,128,140,192,160,174,240,160,175,240,192,208,288,192,209,288,224,243,336,224,244,336,256,278,384,256,279,384,320,348,480,320,349,480,384,417,576,384,418,576,448,487,672,448,488,672,512,557,768,512,558,768,640,696,960,640,697,960,768,835,1152,768,836,1152,896,975,1344,896,976,1344,1024,1114,1536,1024,1115,1536,1152,1253,1728,1152,1254,1728,1280,1393,1920,1280,1394,1920][3*o+n];if(s+l>e.length)return-1;const h=e[s+6]>>5;let d=0;2===h?d+=2:(1&h&&1!==h&&(d+=2),4&h&&(d+=2));const c=(e[s+6]<<8|e[s+7])>>12-d&1,u=[2,1,2,3,3,4,4,5][h]+c,f=e[s+5]>>3,g=7&e[s+5],m=new Uint8Array([n<<6|f<<1|g>>2,(3&g)<<6|h<<3|c<<2|o>>4,o<<4&224]),p=i+r*(1536/a*9e4),v=e.subarray(s,s+l);return t.config=m,t.channelCount=u,t.samplerate=a,t.samples.push({unit:v,pts:p}),l}class Qi{constructor(){this.VideoSample=null}createVideoSample(t,e,s,i){return{key:t,frame:!1,pts:e,dts:s,units:[],debug:i,length:0}}getLastNalUnit(t){var e;let s,i=this.VideoSample;if(i&&0!==i.units.length||(i=t[t.length-1]),null!=(e=i)&&e.units){const t=i.units;s=t[t.length-1]}return s}pushAccessUnit(t,e){if(t.units.length&&t.frame){if(void 0===t.pts){const s=e.samples,i=s.length;if(!i)return void e.dropped++;{const e=s[i-1];t.pts=e.pts,t.dts=e.dts}}e.samples.push(t)}t.debug.length&&A.log(t.pts+"/"+t.dts+":"+t.debug)}}class Ji{constructor(t){this.data=void 0,this.bytesAvailable=void 0,this.word=void 0,this.bitsAvailable=void 0,this.data=t,this.bytesAvailable=t.byteLength,this.word=0,this.bitsAvailable=0}loadWord(){const t=this.data,e=this.bytesAvailable,s=t.byteLength-e,i=new Uint8Array(4),r=Math.min(4,e);if(0===r)throw new Error("no bytes available");i.set(t.subarray(s,s+r)),this.word=new DataView(i.buffer).getUint32(0),this.bitsAvailable=8*r,this.bytesAvailable-=r}skipBits(t){let e;t=Math.min(t,8*this.bytesAvailable+this.bitsAvailable),this.bitsAvailable>t?(this.word<<=t,this.bitsAvailable-=t):(e=(t-=this.bitsAvailable)>>3,t-=e<<3,this.bytesAvailable-=e,this.loadWord(),this.word<<=t,this.bitsAvailable-=t)}readBits(t){let e=Math.min(this.bitsAvailable,t);const s=this.word>>>32-e;if(t>32&&A.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0)this.word<<=e;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return e=t-e,e>0&&this.bitsAvailable?s<>>t)return this.word<<=t,this.bitsAvailable-=t,t;return this.loadWord(),t+this.skipLZ()}skipUEG(){this.skipBits(1+this.skipLZ())}skipEG(){this.skipBits(1+this.skipLZ())}readUEG(){const t=this.skipLZ();return this.readBits(t+1)-1}readEG(){const t=this.readUEG();return 1&t?1+t>>>1:-1*(t>>>1)}readBoolean(){return 1===this.readBits(1)}readUByte(){return this.readBits(8)}readUShort(){return this.readBits(16)}readUInt(){return this.readBits(32)}skipScalingList(t){let e,s=8,i=8;for(let r=0;r{var n;switch(i.type){case 1:{let e=!1;a=!0;const r=i.data;if(l&&r.length>4){const t=new Ji(r).readSliceType();2!==t&&4!==t&&7!==t&&9!==t||(e=!0)}var h;if(e)null!=(h=o)&&h.frame&&!o.key&&(this.pushAccessUnit(o,t),o=this.VideoSample=null);o||(o=this.VideoSample=this.createVideoSample(!0,s.pts,s.dts,"")),o.frame=!0,o.key=e;break}case 5:a=!0,null!=(n=o)&&n.frame&&!o.key&&(this.pushAccessUnit(o,t),o=this.VideoSample=null),o||(o=this.VideoSample=this.createVideoSample(!0,s.pts,s.dts,"")),o.key=!0,o.frame=!0;break;case 6:a=!0,Kt(i.data,1,s.pts,e.samples);break;case 7:{var d,c;a=!0,l=!0;const e=i.data,s=new Ji(e).readSPS();if(!t.sps||t.width!==s.width||t.height!==s.height||(null==(d=t.pixelRatio)?void 0:d[0])!==s.pixelRatio[0]||(null==(c=t.pixelRatio)?void 0:c[1])!==s.pixelRatio[1]){t.width=s.width,t.height=s.height,t.pixelRatio=s.pixelRatio,t.sps=[e],t.duration=r;const i=e.subarray(1,4);let n="avc1.";for(let t=0;t<3;t++){let e=i[t].toString(16);e.length<2&&(e="0"+e),n+=e}t.codec=n}break}case 8:a=!0,t.pps=[i.data];break;case 9:a=!0,t.audFound=!0,o&&this.pushAccessUnit(o,t),o=this.VideoSample=this.createVideoSample(!1,s.pts,s.dts,"");break;case 12:a=!0;break;default:a=!1,o&&(o.debug+="unknown NAL "+i.type+" ")}if(o&&a){o.units.push(i)}})),i&&o&&(this.pushAccessUnit(o,t),this.VideoSample=null)}parseAVCNALu(t,e){const s=e.byteLength;let i=t.naluState||0;const r=i,n=[];let a,o,l,h=0,d=-1,c=0;for(-1===i&&(d=0,c=31&e[0],i=0,h=1);h=0){const t={data:e.subarray(d,o),type:c};n.push(t)}else{const s=this.getLastNalUnit(t.samples);s&&(r&&h<=4-r&&s.state&&(s.data=s.data.subarray(0,s.data.byteLength-r)),o>0&&(s.data=Bt(s.data,e.subarray(0,o)),s.state=0))}h=0&&i>=0){const t={data:e.subarray(d,s),type:c,state:i};n.push(t)}if(0===n.length){const s=this.getLastNalUnit(t.samples);s&&(s.data=Bt(s.data,e))}return t.naluState=i,n}}class tr{constructor(t,e,s){this.keyData=void 0,this.decrypter=void 0,this.keyData=s,this.decrypter=new di(e,{removePKCS7Padding:!1})}decryptBuffer(t){return this.decrypter.decrypt(t,this.keyData.key.buffer,this.keyData.iv.buffer)}decryptAacSample(t,e,s){const i=t[e].unit;if(i.length<=16)return;const r=i.subarray(16,i.length-i.length%16),n=r.buffer.slice(r.byteOffset,r.byteOffset+r.length);this.decryptBuffer(n).then((r=>{const n=new Uint8Array(r);i.set(n,16),this.decrypter.isSync()||this.decryptAacSamples(t,e+1,s)}))}decryptAacSamples(t,e,s){for(;;e++){if(e>=t.length)return void s();if(!(t[e].unit.length<32)&&(this.decryptAacSample(t,e,s),!this.decrypter.isSync()))return}}getAvcEncryptedData(t){const e=16*Math.floor((t.length-48)/160)+16,s=new Int8Array(e);let i=0;for(let e=32;e{r.data=this.getAvcDecryptedUnit(n,a),this.decrypter.isSync()||this.decryptAvcSamples(t,e,s+1,i)}))}decryptAvcSamples(t,e,s,i){if(t instanceof Uint8Array)throw new Error("Cannot decrypt samples of type Uint8Array");for(;;e++,s=0){if(e>=t.length)return void i();const r=t[e].units;for(;!(s>=r.length);s++){const n=r[s];if(!(n.data.length<=48||1!==n.type&&5!==n.type||(this.decryptAvcSample(t,e,s,i,n),this.decrypter.isSync())))return}}}}const er=188;class sr{constructor(t,e,s){this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.sampleAes=null,this.pmtParsed=!1,this.audioCodec=void 0,this.videoCodec=void 0,this._duration=0,this._pmtId=-1,this._videoTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.remainderData=null,this.videoParser=void 0,this.observer=t,this.config=e,this.typeSupported=s,this.videoParser=new Zi}static probe(t){const e=sr.syncOffset(t);return e>0&&A.warn(`MPEG2-TS detected but first sync word found @ offset ${e}`),-1!==e}static syncOffset(t){const e=t.length;let s=Math.min(940,e-er)+1,i=0;for(;i1&&(0===n&&a>2||o+er>s))return n}i++}return-1}static createTrack(t,e){return{container:"video"===t||"audio"===t?"video/mp2t":void 0,type:t,id:At[t],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===t?e:void 0}}resetInitSegment(t,e,s,i){this.pmtParsed=!1,this._pmtId=-1,this._videoTrack=sr.createTrack("video"),this._audioTrack=sr.createTrack("audio",i),this._id3Track=sr.createTrack("id3"),this._txtTrack=sr.createTrack("text"),this._audioTrack.segmentCodec="aac",this.aacOverFlow=null,this.remainderData=null,this.audioCodec=e,this.videoCodec=s,this._duration=i}resetTimeStamp(){}resetContiguity(){const{_audioTrack:t,_videoTrack:e,_id3Track:s}=this;t&&(t.pesData=null),e&&(e.pesData=null),s&&(s.pesData=null),this.aacOverFlow=null,this.remainderData=null}demux(t,e,s=!1,i=!1){let r;s||(this.sampleAes=null);const n=this._videoTrack,a=this._audioTrack,o=this._id3Track,l=this._txtTrack;let h=n.pid,d=n.pesData,c=a.pid,u=o.pid,f=a.pesData,g=o.pesData,m=null,p=this.pmtParsed,v=this._pmtId,y=t.length;if(this.remainderData&&(y=(t=Bt(this.remainderData,t)).length,this.remainderData=null),y>4>1){if(T=e+5+t[e+4],T===e+er)continue}else T=e+4;switch(y){case h:i&&(d&&(r=lr(d))&&this.videoParser.parseAVCPES(n,l,r,!1,this._duration),d={data:[],size:0}),d&&(d.data.push(t.subarray(T,e+er)),d.size+=e+er-T);break;case c:if(i){if(f&&(r=lr(f)))switch(a.segmentCodec){case"aac":this.parseAACPES(a,r);break;case"mp3":this.parseMPEGPES(a,r);break;case"ac3":this.parseAC3PES(a,r)}f={data:[],size:0}}f&&(f.data.push(t.subarray(T,e+er)),f.size+=e+er-T);break;case u:i&&(g&&(r=lr(g))&&this.parseID3PES(o,r),g={data:[],size:0}),g&&(g.data.push(t.subarray(T,e+er)),g.size+=e+er-T);break;case 0:i&&(T+=t[T]+1),v=this._pmtId=rr(t,T);break;case v:{i&&(T+=t[T]+1);const r=nr(t,T,this.typeSupported,s,this.observer);h=r.videoPid,h>0&&(n.pid=h,n.segmentCodec=r.segmentVideoCodec),c=r.audioPid,c>0&&(a.pid=c,a.segmentCodec=r.segmentAudioCodec),u=r.id3Pid,u>0&&(o.pid=u),null===m||p||(A.warn(`MPEG-TS PMT found at ${e} after unknown PID '${m}'. Backtracking to sync byte @${E} to parse all TS packets.`),m=null,e=E-188),p=this.pmtParsed=!0;break}case 17:case 8191:break;default:m=y}}else T++;T>0&&ar(this.observer,new Error(`Found ${T} TS packet/s that do not start with 0x47`)),n.pesData=d,a.pesData=f,o.pesData=g;const S={audioTrack:a,videoTrack:n,id3Track:o,textTrack:l};return i&&this.extractRemainingSamples(S),S}flush(){const{remainderData:t}=this;let e;return this.remainderData=null,e=t?this.demux(t,-1,!1,!0):{videoTrack:this._videoTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(e),this.sampleAes?this.decrypt(e,this.sampleAes):e}extractRemainingSamples(t){const{audioTrack:e,videoTrack:s,id3Track:i,textTrack:r}=t,n=s.pesData,a=e.pesData,o=i.pesData;let l;if(n&&(l=lr(n))?(this.videoParser.parseAVCPES(s,r,l,!0,this._duration),s.pesData=null):s.pesData=n,a&&(l=lr(a))){switch(e.segmentCodec){case"aac":this.parseAACPES(e,l);break;case"mp3":this.parseMPEGPES(e,l);break;case"ac3":this.parseAC3PES(e,l)}e.pesData=null}else null!=a&&a.size&&A.log("last AAC PES packet truncated,might overlap between fragments"),e.pesData=a;o&&(l=lr(o))?(this.parseID3PES(i,l),i.pesData=null):i.pesData=o}demuxSampleAes(t,e,s){const i=this.demux(t,s,!0,!this.config.progressive),r=this.sampleAes=new tr(this.observer,this.config,e);return this.decrypt(i,r)}decrypt(t,e){return new Promise((s=>{const{audioTrack:i,videoTrack:r}=t;i.samples&&"aac"===i.segmentCodec?e.decryptAacSamples(i.samples,0,(()=>{r.samples?e.decryptAvcSamples(r.samples,0,0,(()=>{s(t)})):s(t)})):r.samples&&e.decryptAvcSamples(r.samples,0,0,(()=>{s(t)}))}))}destroy(){this._duration=0}parseAACPES(t,e){let s=0;const i=this.aacOverFlow;let r,n,a,o=e.data;if(i){this.aacOverFlow=null;const e=i.missing,r=i.sample.unit.byteLength;if(-1===e)o=Bt(i.sample.unit,o);else{const n=r-e;i.sample.unit.set(o.subarray(0,e),n),t.samples.push(i.sample),s=i.missing}}for(r=s,n=o.length;r0;)o+=n}}parseID3PES(t,e){if(void 0===e.pts)return void A.warn("[tsdemuxer]: ID3 PES unknown PTS");const s=u({},e,{type:this._videoTrack?Ge:Be,duration:Number.POSITIVE_INFINITY});t.samples.push(s)}}function ir(t,e){return((31&t[e+1])<<8)+t[e+2]}function rr(t,e){return(31&t[e+10])<<8|t[e+11]}function nr(t,e,s,i,r){const n={audioPid:-1,videoPid:-1,id3Pid:-1,segmentVideoCodec:"avc",segmentAudioCodec:"aac"},a=e+3+((15&t[e+1])<<8|t[e+2])-4;for(e+=12+((15&t[e+10])<<8|t[e+11]);e0){let i=e+5,r=o;for(;r>2;){if(106===t[i])!0!==s.ac3?A.log("AC-3 audio found, not supported in this browser for now"):(n.audioPid=a,n.segmentAudioCodec="ac3");const e=t[i+1]+2;i+=e,r-=e}}break;case 194:case 135:return ar(r,new Error("Unsupported EC-3 in M2TS found")),n;case 36:return ar(r,new Error("Unsupported HEVC in M2TS found")),n}e+=o+5}return n}function ar(t,e,s){A.warn(`parsing error: ${e.message}`),t.emit(p.ERROR,p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,fatal:!1,levelRetry:s,error:e,reason:e.message})}function or(t){A.log(`${t} with AES-128-CBC encryption found in unencrypted stream`)}function lr(t){let e,s,i,r,n,a=0;const o=t.data;if(!t||0===t.size)return null;for(;o[0].length<19&&o.length>1;)o[0]=Bt(o[0],o[1]),o.splice(1,1);e=o[0];if(1===(e[0]<<16)+(e[1]<<8)+e[2]){if(s=(e[4]<<8)+e[5],s&&s>t.size-6)return null;const l=e[7];192&l&&(r=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&l?(n=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2,r-n>54e5&&(A.warn(`${Math.round((r-n)/9e4)}s delta between PTS and DTS, align them`),r=n)):n=r),i=e[8];let h=i+9;if(t.size<=h)return null;t.size-=h;const d=new Uint8Array(t.size);for(let t=0,s=o.length;ts){h-=s;continue}e=e.subarray(h),s-=h,h=0}d.set(e,a),a+=s}return s&&(s-=i+3),{data:d,pts:r,dts:n,len:s}}return null}class hr{static getSilentFrame(t,e){if("mp4a.40.2"===t){if(1===e)return new Uint8Array([0,200,0,128,35,128]);if(2===e)return new Uint8Array([33,0,73,144,2,25,0,35,128]);if(3===e)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,142]);if(4===e)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,128,44,128,8,2,56]);if(5===e)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,56]);if(6===e)return new Uint8Array([0,200,0,128,32,132,1,38,64,8,100,0,130,48,4,153,0,33,144,2,0,178,0,32,8,224])}else{if(1===e)return new Uint8Array([1,64,34,128,163,78,230,128,186,8,0,0,0,28,6,241,193,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(2===e)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94]);if(3===e)return new Uint8Array([1,64,34,128,163,94,230,128,186,8,0,0,0,0,149,0,6,241,161,10,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,94])}}}const dr=Math.pow(2,32)-1;class cr{static init(){let t;for(t in cr.types={avc1:[],avcC:[],btrt:[],dinf:[],dref:[],esds:[],ftyp:[],hdlr:[],mdat:[],mdhd:[],mdia:[],mfhd:[],minf:[],moof:[],moov:[],mp4a:[],".mp3":[],dac3:[],"ac-3":[],mvex:[],mvhd:[],pasp:[],sdtp:[],stbl:[],stco:[],stsc:[],stsd:[],stsz:[],stts:[],tfdt:[],tfhd:[],traf:[],trak:[],trun:[],trex:[],tkhd:[],vmhd:[],smhd:[]},cr.types)cr.types.hasOwnProperty(t)&&(cr.types[t]=[t.charCodeAt(0),t.charCodeAt(1),t.charCodeAt(2),t.charCodeAt(3)]);const e=new Uint8Array([0,0,0,0,0,0,0,0,118,105,100,101,0,0,0,0,0,0,0,0,0,0,0,0,86,105,100,101,111,72,97,110,100,108,101,114,0]),s=new Uint8Array([0,0,0,0,0,0,0,0,115,111,117,110,0,0,0,0,0,0,0,0,0,0,0,0,83,111,117,110,100,72,97,110,100,108,101,114,0]);cr.HDLR_TYPES={video:e,audio:s};const i=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1]),r=new Uint8Array([0,0,0,0,0,0,0,0]);cr.STTS=cr.STSC=cr.STCO=r,cr.STSZ=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0]),cr.VMHD=new Uint8Array([0,0,0,1,0,0,0,0,0,0,0,0]),cr.SMHD=new Uint8Array([0,0,0,0,0,0,0,0]),cr.STSD=new Uint8Array([0,0,0,0,0,0,0,1]);const n=new Uint8Array([105,115,111,109]),a=new Uint8Array([97,118,99,49]),o=new Uint8Array([0,0,0,1]);cr.FTYP=cr.box(cr.types.ftyp,n,o,n,a),cr.DINF=cr.box(cr.types.dinf,cr.box(cr.types.dref,i))}static box(t,...e){let s=8,i=e.length;const r=i;for(;i--;)s+=e[i].byteLength;const n=new Uint8Array(s);for(n[0]=s>>24&255,n[1]=s>>16&255,n[2]=s>>8&255,n[3]=255&s,n.set(t,4),i=0,s=8;i>24&255,t>>16&255,t>>8&255,255&t,s>>24,s>>16&255,s>>8&255,255&s,i>>24,i>>16&255,i>>8&255,255&i,85,196,0,0]))}static mdia(t){return cr.box(cr.types.mdia,cr.mdhd(t.timescale,t.duration),cr.hdlr(t.type),cr.minf(t))}static mfhd(t){return cr.box(cr.types.mfhd,new Uint8Array([0,0,0,0,t>>24,t>>16&255,t>>8&255,255&t]))}static minf(t){return"audio"===t.type?cr.box(cr.types.minf,cr.box(cr.types.smhd,cr.SMHD),cr.DINF,cr.stbl(t)):cr.box(cr.types.minf,cr.box(cr.types.vmhd,cr.VMHD),cr.DINF,cr.stbl(t))}static moof(t,e,s){return cr.box(cr.types.moof,cr.mfhd(t),cr.traf(s,e))}static moov(t){let e=t.length;const s=[];for(;e--;)s[e]=cr.trak(t[e]);return cr.box.apply(null,[cr.types.moov,cr.mvhd(t[0].timescale,t[0].duration)].concat(s).concat(cr.mvex(t)))}static mvex(t){let e=t.length;const s=[];for(;e--;)s[e]=cr.trex(t[e]);return cr.box.apply(null,[cr.types.mvex,...s])}static mvhd(t,e){e*=t;const s=Math.floor(e/(dr+1)),i=Math.floor(e%(dr+1)),r=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,t>>24&255,t>>16&255,t>>8&255,255&t,s>>24,s>>16&255,s>>8&255,255&s,i>>24,i>>16&255,i>>8&255,255&i,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return cr.box(cr.types.mvhd,r)}static sdtp(t){const e=t.samples||[],s=new Uint8Array(4+e.length);let i,r;for(i=0;i>>8&255),r.push(255&i),r=r.concat(Array.prototype.slice.call(s));for(e=0;e>>8&255),n.push(255&i),n=n.concat(Array.prototype.slice.call(s));const a=cr.box(cr.types.avcC,new Uint8Array([1,r[3],r[4],r[5],255,224|t.sps.length].concat(r).concat([t.pps.length]).concat(n))),o=t.width,l=t.height,h=t.pixelRatio[0],d=t.pixelRatio[1];return cr.box(cr.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,o>>8&255,255&o,l>>8&255,255&l,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),a,cr.box(cr.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),cr.box(cr.types.pasp,new Uint8Array([h>>24,h>>16&255,h>>8&255,255&h,d>>24,d>>16&255,d>>8&255,255&d])))}static esds(t){const e=t.config.length;return new Uint8Array([0,0,0,0,3,23+e,0,1,0,4,15+e,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([e]).concat(t.config).concat([6,1,2]))}static audioStsd(t){const e=t.samplerate;return new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t.channelCount,0,16,0,0,0,0,e>>8&255,255&e,0,0])}static mp4a(t){return cr.box(cr.types.mp4a,cr.audioStsd(t),cr.box(cr.types.esds,cr.esds(t)))}static mp3(t){return cr.box(cr.types[".mp3"],cr.audioStsd(t))}static ac3(t){return cr.box(cr.types["ac-3"],cr.audioStsd(t),cr.box(cr.types.dac3,t.config))}static stsd(t){return"audio"===t.type?"mp3"===t.segmentCodec&&"mp3"===t.codec?cr.box(cr.types.stsd,cr.STSD,cr.mp3(t)):"ac3"===t.segmentCodec?cr.box(cr.types.stsd,cr.STSD,cr.ac3(t)):cr.box(cr.types.stsd,cr.STSD,cr.mp4a(t)):cr.box(cr.types.stsd,cr.STSD,cr.avc1(t))}static tkhd(t){const e=t.id,s=t.duration*t.timescale,i=t.width,r=t.height,n=Math.floor(s/(dr+1)),a=Math.floor(s%(dr+1));return cr.box(cr.types.tkhd,new Uint8Array([1,0,0,7,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,e>>24&255,e>>16&255,e>>8&255,255&e,0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n,a>>24,a>>16&255,a>>8&255,255&a,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,i>>8&255,255&i,0,0,r>>8&255,255&r,0,0]))}static traf(t,e){const s=cr.sdtp(t),i=t.id,r=Math.floor(e/(dr+1)),n=Math.floor(e%(dr+1));return cr.box(cr.types.traf,cr.box(cr.types.tfhd,new Uint8Array([0,0,0,0,i>>24,i>>16&255,i>>8&255,255&i])),cr.box(cr.types.tfdt,new Uint8Array([1,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,n>>24,n>>16&255,n>>8&255,255&n])),cr.trun(t,s.length+16+20+8+16+8+8),s)}static trak(t){return t.duration=t.duration||4294967295,cr.box(cr.types.trak,cr.tkhd(t),cr.mdia(t))}static trex(t){const e=t.id;return cr.box(cr.types.trex,new Uint8Array([0,0,0,0,e>>24,e>>16&255,e>>8&255,255&e,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))}static trun(t,e){const s=t.samples||[],i=s.length,r=12+16*i,n=new Uint8Array(r);let a,o,l,h,d,c;for(e+=8+r,n.set(["video"===t.type?1:0,0,15,1,i>>>24&255,i>>>16&255,i>>>8&255,255&i,e>>>24&255,e>>>16&255,e>>>8&255,255&e],0),a=0;a>>24&255,l>>>16&255,l>>>8&255,255&l,h>>>24&255,h>>>16&255,h>>>8&255,255&h,d.isLeading<<2|d.dependsOn,d.isDependedOn<<6|d.hasRedundancy<<4|d.paddingValue<<1|d.isNonSync,61440&d.degradPrio,15&d.degradPrio,c>>>24&255,c>>>16&255,c>>>8&255,255&c],12+16*a);return cr.box(cr.types.trun,n)}static initSegment(t){cr.types||cr.init();const e=cr.moov(t);return Bt(cr.FTYP,e)}}cr.types=void 0,cr.HDLR_TYPES=void 0,cr.STTS=void 0,cr.STSC=void 0,cr.STCO=void 0,cr.STSZ=void 0,cr.VMHD=void 0,cr.SMHD=void 0,cr.STSD=void 0,cr.FTYP=void 0,cr.DINF=void 0;function ur(t,e,s=1,i=!1){const r=t*e*s;return i?Math.round(r):r}function fr(t,e=!1){return ur(t,1e3,1/9e4,e)}let gr,mr=null,pr=null;class vr{constructor(t,e,s,i=""){if(this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.ISGenerated=!1,this._initPTS=null,this._initDTS=null,this.nextAvcDts=null,this.nextAudioPts=null,this.videoSampleDuration=null,this.isAudioContiguous=!1,this.isVideoContiguous=!1,this.videoTrackConfig=void 0,this.observer=t,this.config=e,this.typeSupported=s,this.ISGenerated=!1,null===mr){const t=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);mr=t?parseInt(t[1]):0}if(null===pr){const t=navigator.userAgent.match(/Safari\/(\d+)/i);pr=t?parseInt(t[1]):0}}destroy(){this.config=this.videoTrackConfig=this._initPTS=this._initDTS=null}resetTimeStamp(t){A.log("[mp4-remuxer]: initPTS & initDTS reset"),this._initPTS=this._initDTS=t}resetNextTimestamp(){A.log("[mp4-remuxer]: reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1}resetInitSegment(){A.log("[mp4-remuxer]: ISGenerated flag reset"),this.ISGenerated=!1,this.videoTrackConfig=void 0}getVideoStartPts(t){let e=!1;const s=t.reduce(((t,s)=>{const i=s.pts-t;return i<-4294967296?(e=!0,yr(t,s.pts)):i>0?t:s.pts}),t[0].pts);return e&&A.debug("PTS rollover detected"),s}remux(t,e,s,i,r,n,a,o){let l,h,d,c,u,f,g=r,m=r;const p=t.pid>-1,v=e.pid>-1,y=e.samples.length,E=t.samples.length>0,T=a&&y>0||y>1;if((!p||E)&&(!v||T)||this.ISGenerated||a){if(this.ISGenerated){var S,L,R,b;const t=this.videoTrackConfig;!t||e.width===t.width&&e.height===t.height&&(null==(S=e.pixelRatio)?void 0:S[0])===(null==(L=t.pixelRatio)?void 0:L[0])&&(null==(R=e.pixelRatio)?void 0:R[1])===(null==(b=t.pixelRatio)?void 0:b[1])||this.resetInitSegment()}else d=this.generateIS(t,e,r,n);const s=this.isVideoContiguous;let i,a=-1;if(T&&(a=function(t){for(let e=0;e0){A.warn(`[mp4-remuxer]: Dropped ${a} out of ${y} video samples due to a missing keyframe`);const t=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(a),e.dropped+=a,m+=(e.samples[0].pts-t)/e.inputTimeScale,i=m}else-1===a&&(A.warn(`[mp4-remuxer]: No keyframe found out of ${y} video samples`),f=!1);if(this.ISGenerated){if(E&&T){const s=this.getVideoStartPts(e.samples),i=(yr(t.samples[0].pts,s)-s)/e.inputTimeScale;g+=Math.max(0,i),m+=Math.max(0,-i)}if(E){if(t.samplerate||(A.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),d=this.generateIS(t,e,r,n)),h=this.remuxAudio(t,g,this.isAudioContiguous,n,v||T||o===Ie?m:void 0),T){const i=h?h.endPTS-h.startPTS:0;e.inputTimeScale||(A.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),d=this.generateIS(t,e,r,n)),l=this.remuxVideo(e,m,s,i)}}else T&&(l=this.remuxVideo(e,m,s,0));l&&(l.firstKeyFrame=a,l.independent=-1!==a,l.firstKeyFramePTS=i)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(s.samples.length&&(u=Er(s,r,this._initPTS,this._initDTS)),i.samples.length&&(c=Tr(i,r,this._initPTS))),{audio:h,video:l,initSegment:d,independent:f,text:c,id3:u}}generateIS(t,e,s,i){const r=t.samples,n=e.samples,a=this.typeSupported,o={},l=this._initPTS;let h,d,c,u=!l||i,f="audio/mp4";if(u&&(h=d=1/0),t.config&&r.length){switch(t.timescale=t.samplerate,t.segmentCodec){case"mp3":a.mpeg?(f="audio/mpeg",t.codec=""):a.mp3&&(t.codec="mp3");break;case"ac3":t.codec="ac-3"}o.audio={id:"audio",container:f,codec:t.codec,initSegment:"mp3"===t.segmentCodec&&a.mpeg?new Uint8Array(0):cr.initSegment([t]),metadata:{channelCount:t.channelCount}},u&&(c=t.inputTimeScale,l&&c===l.timescale?u=!1:h=d=r[0].pts-Math.round(c*s))}if(e.sps&&e.pps&&n.length){if(e.timescale=e.inputTimeScale,o.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:cr.initSegment([e]),metadata:{width:e.width,height:e.height}},u)if(c=e.inputTimeScale,l&&c===l.timescale)u=!1;else{const t=this.getVideoStartPts(n),e=Math.round(c*s);d=Math.min(d,yr(n[0].dts,t)-e),h=Math.min(h,t-e)}this.videoTrackConfig={width:e.width,height:e.height,pixelRatio:e.pixelRatio}}if(Object.keys(o).length)return this.ISGenerated=!0,u?(this._initPTS={baseTime:h,timescale:c},this._initDTS={baseTime:d,timescale:c}):h=c=void 0,{tracks:o,initPTS:h,timescale:c}}remuxVideo(t,e,s,i){const r=t.inputTimeScale,n=t.samples,a=[],o=n.length,l=this._initPTS;let h,d,c=this.nextAvcDts,f=8,g=this.videoSampleDuration,m=Number.POSITIVE_INFINITY,E=Number.NEGATIVE_INFINITY,T=!1;if(!s||null===c){const t=e*r,i=n[0].pts-yr(n[0].dts,n[0].pts);mr&&null!==c&&Math.abs(t-i-c)<15e3?s=!0:c=t-i}const S=l.baseTime*r/l.timescale;for(let t=0;t0?t-1:t].dts&&(T=!0)}T&&n.sort((function(t,e){const s=t.dts-e.dts,i=t.pts-e.pts;return s||i})),h=n[0].dts,d=n[n.length-1].dts;const L=d-h,R=L?Math.round(L/(o-1)):g||t.inputTimeScale/30;if(s){const t=h-c,s=t>R,i=t<-1;if((s||i)&&(s?A.warn(`AVC: ${fr(t,!0)} ms (${t}dts) hole between fragments detected at ${e.toFixed(3)}`):A.warn(`AVC: ${fr(-t,!0)} ms (${t}dts) overlapping between fragments detected at ${e.toFixed(3)}`),!i||c>=n[0].pts||mr)){h=c;const e=n[0].pts-t;if(s)n[0].dts=h,n[0].pts=e;else for(let s=0;se);s++)n[s].dts-=t,n[s].pts-=t;A.log(`Video: Initial PTS/DTS adjusted: ${fr(e,!0)}/${fr(h,!0)}, delta: ${fr(t,!0)} ms`)}}h=Math.max(0,h);let b=0,k=0,w=h;for(let t=0;t0?e.dts-n[t-1].dts:R;if(l=t>0?e.pts-n[t-1].pts:R,s.stretchShortVideoTrack&&null!==this.nextAudioPts){const t=Math.floor(s.maxBufferHole*r),n=(i?m+i*r:this.nextAudioPts)-e.pts;n>t?(g=n-a,g<0?g=a:C=!0,A.log(`[mp4-remuxer]: It is approximately ${n/90} ms to the next segment; using duration ${g/90} ms for the last video frame.`)):g=a}else g=a}const d=Math.round(e.pts-e.dts);x=Math.min(x,g),M=Math.max(M,g),P=Math.min(P,l),F=Math.max(F,l),a.push(new Sr(e.key,g,h,d))}if(a.length)if(mr){if(mr<70){const t=a[0].flags;t.dependsOn=2,t.isNonSync=0}}else if(pr&&F-P0&&(i&&Math.abs(T-E)<9e3||Math.abs(yr(g[0].pts-S,T)-E)<20*l),g.forEach((function(t){t.pts=yr(t.pts-S,T)})),!s||E<0){if(g=g.filter((t=>t.pts>=0)),!g.length)return;E=0===r?0:i&&!f?Math.max(0,T):g[0].pts}if("aac"===t.segmentCodec){const e=this.config.maxAudioFramesDrift;for(let s=0,i=E;s=e*l&&h<1e4&&f){let e=Math.round(o/l);i=a-e*l,i<0&&(e--,i+=l),0===s&&(this.nextAudioPts=E=i),A.warn(`[mp4-remuxer]: Injecting ${e} audio frame @ ${(i/n).toFixed(3)}s due to ${Math.round(1e3*o/n)} ms gap.`);for(let n=0;n0))return;k+=m;try{L=new Uint8Array(k)}catch(t){return void this.observer.emit(p.ERROR,p.ERROR,{type:v.MUX_ERROR,details:y.REMUX_ALLOC_ERROR,fatal:!1,error:t,bytes:k,reason:`fail allocating audio mdat ${k}`})}if(!d){new DataView(L.buffer).setUint32(0,k),L.set(cr.types.mdat,4)}}L.set(r,m);const l=r.byteLength;m+=l,c.push(new Sr(!0,o,l,0)),b=n}const D=c.length;if(!D)return;const I=c[c.length-1];this.nextAudioPts=E=b+a*I.duration;const _=d?new Uint8Array(0):cr.moof(t.sequenceNumber++,R/a,u({},t,{samples:c}));t.samples=[];const C=R/n,x=E/n,P={data1:_,data2:L,startPTS:C,endPTS:x,startDTS:C,endDTS:x,type:"audio",hasAudio:!0,hasVideo:!1,nb:D};return this.isAudioContiguous=!0,P}remuxEmptyAudio(t,e,s,i){const r=t.inputTimeScale,n=r/(t.samplerate?t.samplerate:r),a=this.nextAudioPts,o=this._initDTS,l=9e4*o.baseTime/o.timescale,h=(null!==a?a:i.startDTS*r)+l,d=i.endDTS*r+l,c=1024*n,u=Math.ceil((d-h)/c),f=hr.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(A.warn("[mp4-remuxer]: remux empty Audio"),!f)return void A.trace("[mp4-remuxer]: Unable to remuxEmptyAudio since we were unable to get a silent frame for given audio codec");const g=[];for(let t=0;t4294967296;)t+=s;return t}function Er(t,e,s,i){const r=t.samples.length;if(!r)return;const n=t.inputTimeScale;for(let a=0;at.pts-e.pts));const n=t.samples;return t.samples=[],{samples:n}}class Sr{constructor(t,e,s,i){this.size=void 0,this.duration=void 0,this.cts=void 0,this.flags=void 0,this.duration=e,this.size=s,this.cts=i,this.flags={isLeading:0,isDependedOn:0,hasRedundancy:0,degradPrio:0,dependsOn:t?2:1,isNonSync:t?0:1}}}function Lr(t,e){const s=null==t?void 0:t.codec;if(s&&s.length>4)return s;if(e===_){if("ec-3"===s||"ac-3"===s||"alac"===s)return s;if("fLaC"===s||"Opus"===s){return he(s,!1)}const t="mp4a.40.5";return A.info(`Parsed audio codec "${s}" or audio object type not handled. Using "${t}"`),t}return A.warn(`Unhandled video codec "${s}"`),"hvc1"===s||"hev1"===s?"hvc1.1.6.L120.90":"av01"===s?"av01.0.04M.08":"avc1.42e01e"}try{gr=self.performance.now.bind(self.performance)}catch(t){A.debug("Unable to use Performance API on this environment"),gr=null==$?void 0:$.Date.now}const Ar=[{demux:class{constructor(t,e){this.remainderData=null,this.timeOffset=0,this.config=void 0,this.videoTrack=void 0,this.audioTrack=void 0,this.id3Track=void 0,this.txtTrack=void 0,this.config=e}resetTimeStamp(){}resetInitSegment(t,e,s,i){const r=this.videoTrack=ki("video",1),n=this.audioTrack=ki("audio",1),a=this.txtTrack=ki("text",1);if(this.id3Track=ki("id3",1),this.timeOffset=0,null==t||!t.byteLength)return;const o=xt(t);if(o.video){const{id:t,timescale:e,codec:s}=o.video;r.id=t,r.timescale=a.timescale=e,r.codec=s}if(o.audio){const{id:t,timescale:e,codec:s}=o.audio;n.id=t,n.timescale=e,n.codec=s}a.id=At.text,r.sampleDuration=0,r.duration=n.duration=i}resetContiguity(){this.remainderData=null}static probe(t){return function(t){const e=t.byteLength;for(let s=0;s8&&109===t[s+4]&&111===t[s+5]&&111===t[s+6]&&102===t[s+7])return!0;s=i>1?s+i:e}return!1}(t)}demux(t,e){this.timeOffset=e;let s=t;const i=this.videoTrack,r=this.txtTrack;if(this.config.progressive){this.remainderData&&(s=Bt(this.remainderData,t));const e=function(t){const e={valid:null,remainder:null},s=_t(t,["moof"]);if(s.length<2)return e.remainder=t,e;const i=s[s.length-1];return e.valid=st(t,0,i.byteOffset-8),e.remainder=st(t,i.byteOffset-8),e}(s);this.remainderData=e.remainder,i.samples=e.valid||new Uint8Array}else i.samples=s;const n=this.extractID3Track(i,e);return r.samples=$t(e,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:n,textTrack:this.txtTrack}}flush(){const t=this.timeOffset,e=this.videoTrack,s=this.txtTrack;e.samples=this.remainderData||new Uint8Array,this.remainderData=null;const i=this.extractID3Track(e,this.timeOffset);return s.samples=$t(t,e),{videoTrack:e,audioTrack:ki(),id3Track:i,textTrack:ki()}}extractID3Track(t,e){const s=this.id3Track;if(t.samples.length){const i=_t(t.samples,["emsg"]);i&&i.forEach((t=>{const i=function(t){const e=t[0];let s="",i="",r=0,n=0,a=0,o=0,l=0,h=0;if(0===e){for(;"\0"!==Rt(t.subarray(h,h+1));)s+=Rt(t.subarray(h,h+1)),h+=1;for(s+=Rt(t.subarray(h,h+1)),h+=1;"\0"!==Rt(t.subarray(h,h+1));)i+=Rt(t.subarray(h,h+1)),h+=1;i+=Rt(t.subarray(h,h+1)),h+=1,r=kt(t,12),n=kt(t,16),o=kt(t,20),l=kt(t,24),h=28}else if(1===e){h+=4,r=kt(t,h),h+=4;const e=kt(t,h);h+=4;const n=kt(t,h);for(h+=4,a=2**32*e+n,g(a)||(a=Number.MAX_SAFE_INTEGER,A.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=kt(t,h),h+=4,l=kt(t,h),h+=4;"\0"!==Rt(t.subarray(h,h+1));)s+=Rt(t.subarray(h,h+1)),h+=1;for(s+=Rt(t.subarray(h,h+1)),h+=1;"\0"!==Rt(t.subarray(h,h+1));)i+=Rt(t.subarray(h,h+1)),h+=1;i+=Rt(t.subarray(h,h+1)),h+=1}return{schemeIdUri:s,value:i,timeScale:r,presentationTime:a,presentationTimeDelta:n,eventDuration:o,id:l,payload:t.subarray(h,t.byteLength)}}(t);if(ji.test(i.schemeIdUri)){const t=f(i.presentationTime)?i.presentationTime/i.timeScale:e+i.presentationTimeDelta/i.timeScale;let r=4294967295===i.eventDuration?Number.POSITIVE_INFINITY:i.eventDuration/i.timeScale;r<=.001&&(r=Number.POSITIVE_INFINITY);const n=i.payload;s.samples.push({data:n,len:n.byteLength,dts:t,pts:t,type:Ge,duration:r})}}))}return s}demuxSampleAes(t,e,s){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))}destroy(){}},remux:class{constructor(){this.emitInitSegment=!1,this.audioCodec=void 0,this.videoCodec=void 0,this.initData=void 0,this.initPTS=null,this.initTracks=void 0,this.lastEndTime=null}destroy(){}resetTimeStamp(t){this.initPTS=t,this.lastEndTime=null}resetNextTimestamp(){this.lastEndTime=null}resetInitSegment(t,e,s,i){this.audioCodec=e,this.videoCodec=s,this.generateInitSegment(function(t,e){if(!t||!e)return t;const s=e.keyId;s&&e.isCommonEncryption&&_t(t,["moov","trak"]).forEach((t=>{const e=_t(t,["mdia","minf","stbl","stsd"])[0].subarray(8);let i=_t(e,["enca"]);const r=i.length>0;r||(i=_t(e,["encv"])),i.forEach((t=>{_t(r?t.subarray(28):t.subarray(78),["sinf"]).forEach((t=>{const e=Nt(t);if(e){const t=e.subarray(8,24);t.some((t=>0!==t))||(A.log(`[eme] Patching keyId in 'enc${r?"a":"v"}>sinf>>tenc' box: ${Tt(t)} -> ${Tt(s)}`),e.set(s,8))}}))}))}));return t}(t,i)),this.emitInitSegment=!0}generateInitSegment(t){let{audioCodec:e,videoCodec:s}=this;if(null==t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);const i=this.initData=xt(t);i.audio&&(e=Lr(i.audio,_)),i.video&&(s=Lr(i.video,C));const r={};i.audio&&i.video?r.audiovideo={container:"video/mp4",codec:e+","+s,initSegment:t,id:"main"}:i.audio?r.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?r.video={container:"video/mp4",codec:s,initSegment:t,id:"main"}:A.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=r}remux(t,e,s,i,r,n){var a,o;let{initPTS:l,lastEndTime:h}=this;const d={audio:void 0,video:void 0,text:i,id3:s,initSegment:void 0};f(h)||(h=this.lastEndTime=r||0);const c=e.samples;if(null==c||!c.length)return d;const u={initPTS:void 0,timescale:1};let g=this.initData;if(null!=(a=g)&&a.length||(this.generateInitSegment(c),g=this.initData),null==(o=g)||!o.length)return A.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),d;this.emitInitSegment&&(u.tracks=this.initTracks,this.emitInitSegment=!1);const m=function(t,e){let s=0,i=0,r=0;const n=_t(t,["moof","traf"]);for(let t=0;tt+e.info.duration||0),0);s=Math.max(s,t+n.earliestPresentationTime/n.timescale),i=s-e}}if(i&&f(i))return i}return i||r}(c,g),p=function(t,e){return _t(e,["moof","traf"]).reduce(((e,s)=>{const i=_t(s,["tfdt"])[0],r=i[0],n=_t(s,["tfhd"]).reduce(((e,s)=>{const n=kt(s,4),a=t[n];if(a){let t=kt(i,4);if(1===r){if(t===St)return A.warn("[mp4-demuxer]: Ignoring assumed invalid signed 64-bit track fragment decode time"),e;t*=St+1,t+=kt(i,8)}const s=t/(a.timescale||9e4);if(f(s)&&(null===e||sr}(l,v,r,m)||u.timescale!==l.timescale&&n)&&(u.initPTS=v-r,l&&1===l.timescale&&A.warn("Adjusting initPTS by "+(u.initPTS-l.baseTime)),this.initPTS=l={baseTime:u.initPTS,timescale:1});const y=t?v-l.baseTime/l.timescale:h,E=y+m;!function(t,e,s){_t(e,["moof","traf"]).forEach((e=>{_t(e,["tfhd"]).forEach((i=>{const r=kt(i,4),n=t[r];if(!n)return;const a=n.timescale||9e4;_t(e,["tfdt"]).forEach((t=>{const e=t[0],i=s*a;if(i){let s=kt(t,4);if(0===e)s-=i,s=Math.max(s,0),It(t,4,s);else{s*=Math.pow(2,32),s+=kt(t,8),s-=i,s=Math.max(s,0);const e=Math.floor(s/(St+1)),r=Math.floor(s%(St+1));It(t,4,e),It(t,8,r)}}}))}))}))}(g,c,l.baseTime/l.timescale),m>0?this.lastEndTime=E:(A.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());const T=!!g.audio,S=!!g.video;let L="";T&&(L+="audio"),S&&(L+="video");const R={data1:c,startPTS:y,startDTS:y,endPTS:E,endDTS:E,type:L,hasAudio:T,hasVideo:S,nb:1,dropped:0};return d.audio="audio"===R.type?R:void 0,d.video="audio"!==R.type?R:void 0,d.initSegment=u,d.id3=Er(s,r,l,l),i.samples.length&&(d.text=Tr(i,r,l)),d}}},{demux:sr,remux:vr},{demux:class extends wi{constructor(t,e){super(),this.observer=void 0,this.config=void 0,this.observer=t,this.config=e}resetInitSegment(t,e,s,i){super.resetInitSegment(t,e,s,i),this._audioTrack={container:"audio/adts",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"aac",samples:[],manifestCodec:e,duration:i,inputTimeScale:9e4,dropped:0}}static probe(t){if(!t)return!1;const e=nt(t,0);let s=(null==e?void 0:e.length)||0;if(Wi(t,s))return!1;for(let e=t.length;s0&&null!=(null==e?void 0:e.key)&&null!==e.iv&&null!=e.method&&(s=e);return s}(n,e);if(L&&"AES-128"===L.method){const t=this.getDecrypter();if(!t.isSync())return this.decryptionPromise=t.webCryptoDecrypt(n,L.key.buffer,L.iv.buffer).then((t=>{const e=this.push(t,null,s);return this.decryptionPromise=null,e})),this.decryptionPromise;{let e=t.softwareDecrypt(n,L.key.buffer,L.iv.buffer);if(s.part>-1&&(e=t.flush()),!e)return r.executeEnd=gr(),br(s);n=new Uint8Array(e)}}const R=this.needsProbing(h,d);if(R){const t=this.configureTransmuxer(n);if(t)return A.warn(`[transmuxer] ${t.message}`),this.observer.emit(p.ERROR,p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,fatal:!1,error:t,reason:t.message}),r.executeEnd=gr(),br(s)}(h||d||f||R)&&this.resetInitSegment(S,g,m,T,e),(h||f||R)&&this.resetInitialTimestamp(E),l||this.resetContiguity();const b=this.transmux(n,L,u,c,s),k=this.currentTransmuxState;return k.contiguous=!0,k.discontinuity=!1,k.trackSwitch=!1,r.executeEnd=gr(),b}flush(t){const e=t.transmuxing;e.executeStart=gr();const{decrypter:s,currentTransmuxState:i,decryptionPromise:r}=this;if(r)return r.then((()=>this.flush(t)));const n=[],{timeOffset:a}=i;if(s){const e=s.flush();e&&n.push(this.push(e,null,t))}const{demuxer:o,remuxer:l}=this;if(!o||!l)return e.executeEnd=gr(),[br(t)];const h=o.flush(a);return kr(h)?h.then((e=>(this.flushRemux(n,e,t),n))):(this.flushRemux(n,h,t),n)}flushRemux(t,e,s){const{audioTrack:i,videoTrack:r,id3Track:n,textTrack:a}=e,{accurateTimeOffset:o,timeOffset:l}=this.currentTransmuxState;A.log(`[transmuxer.ts]: Flushed fragment ${s.sn}${s.part>-1?" p: "+s.part:""} of level ${s.level}`);const h=this.remuxer.remux(i,r,n,a,l,o,!0,this.id);t.push({remuxResult:h,chunkMeta:s}),s.transmuxing.executeEnd=gr()}resetInitialTimestamp(t){const{demuxer:e,remuxer:s}=this;e&&s&&(e.resetTimeStamp(t),s.resetTimeStamp(t))}resetContiguity(){const{demuxer:t,remuxer:e}=this;t&&e&&(t.resetContiguity(),e.resetNextTimestamp())}resetInitSegment(t,e,s,i,r){const{demuxer:n,remuxer:a}=this;n&&a&&(n.resetInitSegment(t,e,s,i),a.resetInitSegment(t,e,s,r))}destroy(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)}transmux(t,e,s,i,r){let n;return n=e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,s,i,r):this.transmuxUnencrypted(t,s,i,r),n}transmuxUnencrypted(t,e,s,i){const{audioTrack:r,videoTrack:n,id3Track:a,textTrack:o}=this.demuxer.demux(t,e,!1,!this.config.progressive);return{remuxResult:this.remuxer.remux(r,n,a,o,e,s,!1,this.id),chunkMeta:i}}transmuxSampleAes(t,e,s,i,r){return this.demuxer.demuxSampleAes(t,e,s).then((t=>({remuxResult:this.remuxer.remux(t.audioTrack,t.videoTrack,t.id3Track,t.textTrack,s,i,!1,this.id),chunkMeta:r})))}configureTransmuxer(t){const{config:e,observer:s,typeSupported:i,vendor:r}=this;let n;for(let e=0,s=Ar.length;e({remuxResult:{},chunkMeta:t});function kr(t){return"then"in t&&t.then instanceof Function}class wr{constructor(t,e,s,i,r){this.audioCodec=void 0,this.videoCodec=void 0,this.initSegmentData=void 0,this.duration=void 0,this.defaultInitPts=void 0,this.audioCodec=t,this.videoCodec=e,this.initSegmentData=s,this.duration=i,this.defaultInitPts=r||null}}class Dr{constructor(t,e,s,i,r,n){this.discontinuity=void 0,this.contiguous=void 0,this.accurateTimeOffset=void 0,this.trackSwitch=void 0,this.timeOffset=void 0,this.initSegmentChange=void 0,this.discontinuity=t,this.contiguous=e,this.accurateTimeOffset=s,this.trackSwitch=i,this.timeOffset=r,this.initSegmentChange=n}}var Ir={exports:{}};!function(t){var e=Object.prototype.hasOwnProperty,s="~";function i(){}function r(t,e,s){this.fn=t,this.context=e,this.once=s||!1}function n(t,e,i,n,a){if("function"!=typeof i)throw new TypeError("The listener must be a function");var o=new r(i,n||t,a),l=s?s+e:e;return t._events[l]?t._events[l].fn?t._events[l]=[t._events[l],o]:t._events[l].push(o):(t._events[l]=o,t._eventsCount++),t}function a(t,e){0==--t._eventsCount?t._events=new i:delete t._events[e]}function o(){this._events=new i,this._eventsCount=0}Object.create&&(i.prototype=Object.create(null),(new i).__proto__||(s=!1)),o.prototype.eventNames=function(){var t,i,r=[];if(0===this._eventsCount)return r;for(i in t=this._events)e.call(t,i)&&r.push(s?i.slice(1):i);return Object.getOwnPropertySymbols?r.concat(Object.getOwnPropertySymbols(t)):r},o.prototype.listeners=function(t){var e=s?s+t:t,i=this._events[e];if(!i)return[];if(i.fn)return[i.fn];for(var r=0,n=i.length,a=new Array(n);r{(e=e||{}).frag=this.frag,e.id=this.id,t===p.ERROR&&(this.error=e.error),this.hls.trigger(t,e)};this.observer=new _r,this.observer.on(p.FRAG_DECRYPTED,n),this.observer.on(p.ERROR,n);const a=te(r.preferManagedMediaSource)||{isTypeSupported:()=>!1},o={mpeg:a.isTypeSupported("audio/mpeg"),mp3:a.isTypeSupported('audio/mp4; codecs="mp3"'),ac3:a.isTypeSupported('audio/mp4; codecs="ac-3"')};if(this.useWorker&&"undefined"!=typeof Worker){if(r.workerPath||"function"==typeof __HLS_WORKER_BUNDLE__){try{r.workerPath?(A.log(`loading Web Worker ${r.workerPath} for "${e}"`),this.workerContext=function(t){const e=new self.URL(t,self.location.href).href;return{worker:new self.Worker(e),scriptURL:e}}(r.workerPath)):(A.log(`injecting Web Worker for "${e}"`),this.workerContext=function(){const t=new self.Blob([`var exports={};var module={exports:exports};function define(f){f()};define.amd=true;(${__HLS_WORKER_BUNDLE__.toString()})(true);`],{type:"text/javascript"}),e=self.URL.createObjectURL(t);return{worker:new self.Worker(e),objectURL:e}}()),this.onwmsg=t=>this.onWorkerMessage(t);const{worker:t}=this.workerContext;t.addEventListener("message",this.onwmsg),t.onerror=t=>{const s=new Error(`${t.message} (${t.filename}:${t.lineno})`);r.enableWorker=!1,A.warn(`Error in "${e}" Web Worker, fallback to inline`),this.hls.trigger(p.ERROR,{type:v.OTHER_ERROR,details:y.INTERNAL_EXCEPTION,fatal:!1,event:"demuxerWorker",error:s})},t.postMessage({cmd:"init",typeSupported:o,vendor:"",id:e,config:JSON.stringify(r)})}catch(t){A.warn(`Error setting up "${e}" Web Worker, fallback to inline`,t),this.resetWorker(),this.error=null,this.transmuxer=new Rr(this.observer,o,r,"",e)}return}}this.transmuxer=new Rr(this.observer,o,r,"",e)}resetWorker(){if(this.workerContext){const{worker:t,objectURL:e}=this.workerContext;e&&self.URL.revokeObjectURL(e),t.removeEventListener("message",this.onwmsg),t.onerror=null,t.terminate(),this.workerContext=null}}destroy(){if(this.workerContext)this.resetWorker(),this.onwmsg=void 0;else{const t=this.transmuxer;t&&(t.destroy(),this.transmuxer=null)}const t=this.observer;t&&t.removeAllListeners(),this.frag=null,this.observer=null,this.hls=null}push(t,e,s,i,r,n,a,o,l,h){var d,c;l.transmuxing.start=self.performance.now();const{transmuxer:u}=this,f=n?n.start:r.start,g=r.decryptdata,m=this.frag,p=!(m&&r.cc===m.cc),v=!(m&&l.level===m.level),y=m?l.sn-m.sn:-1,E=this.part?l.part-this.part.index:-1,T=0===y&&l.id>1&&l.id===(null==m?void 0:m.stats.chunkCount),S=!v&&(1===y||0===y&&(1===E||T&&E<=0)),L=self.performance.now();(v||y||0===r.stats.parsing.start)&&(r.stats.parsing.start=L),!n||!E&&S||(n.stats.parsing.start=L);const R=!(m&&(null==(d=r.initSegment)?void 0:d.url)===(null==(c=m.initSegment)?void 0:c.url)),b=new Dr(p,S,o,v,f,R);if(!S||p||R){A.log(`[transmuxer-interface, ${r.type}]: Starting new transmux session for sn: ${l.sn} p: ${l.part} level: ${l.level} id: ${l.id}\n discontinuity: ${p}\n trackSwitch: ${v}\n contiguous: ${S}\n accurateTimeOffset: ${o}\n timeOffset: ${f}\n initSegmentChange: ${R}`);const t=new wr(s,i,e,a,h);this.configureTransmuxer(t)}if(this.frag=r,this.part=n,this.workerContext)this.workerContext.worker.postMessage({cmd:"demux",data:t,decryptdata:g,chunkMeta:l,state:b},t instanceof ArrayBuffer?[t]:[]);else if(u){const e=u.push(t,g,l,b);kr(e)?(u.async=!0,e.then((t=>{this.handleTransmuxComplete(t)})).catch((t=>{this.transmuxerError(t,l,"transmuxer-interface push error")}))):(u.async=!1,this.handleTransmuxComplete(e))}}flush(t){t.transmuxing.start=self.performance.now();const{transmuxer:e}=this;if(this.workerContext)this.workerContext.worker.postMessage({cmd:"flush",chunkMeta:t});else if(e){let s=e.flush(t);kr(s)||e.async?(kr(s)||(s=Promise.resolve(s)),s.then((e=>{this.handleFlushResult(e,t)})).catch((e=>{this.transmuxerError(e,t,"transmuxer-interface flush error")}))):this.handleFlushResult(s,t)}}transmuxerError(t,e,s){this.hls&&(this.error=t,this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_PARSING_ERROR,chunkMeta:e,frag:this.frag||void 0,fatal:!1,error:t,err:t,reason:s}))}handleFlushResult(t,e){t.forEach((t=>{this.handleTransmuxComplete(t)})),this.onFlush(e)}onWorkerMessage(t){const e=t.data;if(null==e||!e.event)return void A.warn("worker message received with no "+(e?"event name":"data"));const s=this.hls;if(this.hls)switch(e.event){case"init":{var i;const t=null==(i=this.workerContext)?void 0:i.objectURL;t&&self.URL.revokeObjectURL(t);break}case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;case"workerLog":A[e.data.logType]&&A[e.data.logType](e.data.message);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,s.trigger(e.event,e.data)}}configureTransmuxer(t){const{transmuxer:e}=this;this.workerContext?this.workerContext.worker.postMessage({cmd:"configure",config:t}):e&&e.configure(t)}handleTransmuxComplete(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)}}function xr(t,e){if(t.length!==e.length)return!1;for(let s=0;st[s]!==e[s]))}function Mr(t,e){return e.label.toLowerCase()===t.name.toLowerCase()&&(!e.language||e.language.toLowerCase()===(t.lang||"").toLowerCase())}class Fr{constructor(t){this.buffered=void 0;const e=(e,s,i)=>{if((s>>>=0)>i-1)throw new DOMException(`Failed to execute '${e}' on 'TimeRanges': The index provided (${s}) is greater than the maximum bound (${i})`);return t[s][e]};this.buffered={get length(){return t.length},end:s=>e("end",s,t.length),start:s=>e("start",s,t.length)}}}class Or{constructor(t){this.buffers=void 0,this.queues={video:[],audio:[],audiovideo:[]},this.buffers=t}append(t,e,s){const i=this.queues[e];i.push(t),1!==i.length||s||this.executeNext(e)}insertAbort(t,e){this.queues[e].unshift(t),this.executeNext(e)}appendBlocker(t){let e;const s=new Promise((t=>{e=t})),i={execute:e,onStart:()=>{},onComplete:()=>{},onError:()=>{}};return this.append(i,t),s}executeNext(t){const e=this.queues[t];if(e.length){const s=e[0];try{s.execute()}catch(e){A.warn(`[buffer-operation-queue]: Exception executing "${t}" SourceBuffer operation: ${e}`),s.onError(e);const i=this.buffers[t];null!=i&&i.updating||this.shiftAndExecuteNext(t)}}}shiftAndExecuteNext(t){this.queues[t].shift(),this.executeNext(t)}current(t){return this.queues[t][0]}}const Nr=/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/;function Ur(t){const e=t.querySelectorAll("source");[].slice.call(e).forEach((e=>{t.removeChild(e)}))}const Br={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,128:174,129:176,130:189,131:191,132:8482,133:162,134:163,135:9834,136:224,137:32,138:232,139:226,140:234,141:238,142:244,143:251,144:193,145:201,146:211,147:218,148:220,149:252,150:8216,151:161,152:42,153:8217,154:9473,155:169,156:8480,157:8226,158:8220,159:8221,160:192,161:194,162:199,163:200,164:202,165:203,166:235,167:206,168:207,169:239,170:212,171:217,172:249,173:219,174:171,175:187,176:195,177:227,178:205,179:204,180:236,181:210,182:242,183:213,184:245,185:123,186:125,187:92,188:94,189:95,190:124,191:8764,192:196,193:228,194:214,195:246,196:223,197:165,198:164,199:9475,200:197,201:229,202:216,203:248,204:9487,205:9491,206:9495,207:9499},$r=t=>String.fromCharCode(Br[t]||t),Gr=15,Kr=100,Hr={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},Vr={17:2,18:4,21:6,22:8,23:10,19:13,20:15},Yr={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},Wr={25:2,26:4,29:6,30:8,31:10,27:13,28:15},jr=["white","green","blue","cyan","red","yellow","magenta","black","transparent"];class qr{constructor(){this.time=null,this.verboseLevel=0}log(t,e){if(this.verboseLevel>=t){const s="function"==typeof e?e():e;A.log(`${this.time} [${t}] ${s}`)}}}const Xr=function(t){const e=[];for(let s=0;sKr&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=Kr)}moveCursor(t){const e=this.pos+t;if(t>1)for(let t=this.pos+1;t=144&&this.backSpace();const e=$r(t);this.pos>=Kr?this.logger.log(0,(()=>"Cannot insert "+t.toString(16)+" ("+e+") at position "+this.pos+". Skipping it!")):(this.chars[this.pos].setChar(e,this.currPenState),this.moveCursor(1))}clearFromPos(t){let e;for(e=t;e"pacData = "+JSON.stringify(t)));let e=t.row-1;if(this.nrRollUpRows&&e"bkgData = "+JSON.stringify(t))),this.backSpace(),this.setPen(t),this.insertChar(32)}setRollUpRows(t){this.nrRollUpRows=t}rollUp(){if(null===this.nrRollUpRows)return void this.logger.log(3,"roll_up but nrRollUpRows not set yet");this.logger.log(1,(()=>this.getDisplayText()));const t=this.currRow+1-this.nrRollUpRows,e=this.rows.splice(t,1)[0];e.clear(),this.rows.splice(this.currRow,0,e),this.logger.log(2,"Rolling up")}getDisplayText(t){t=t||!1;const e=[];let s="",i=-1;for(let s=0;s0&&(s=t?"["+e.join(" | ")+"]":e.join("\n")),s}getTextAndFormat(){return this.rows}}class tn{constructor(t,e,s){this.chNr=void 0,this.outputFilter=void 0,this.mode=void 0,this.verbose=void 0,this.displayedMemory=void 0,this.nonDisplayedMemory=void 0,this.lastOutputScreen=void 0,this.currRollUpRow=void 0,this.writeScreen=void 0,this.cueStartTime=void 0,this.logger=void 0,this.chNr=t,this.outputFilter=e,this.mode=null,this.verbose=0,this.displayedMemory=new Zr(s),this.nonDisplayedMemory=new Zr(s),this.lastOutputScreen=new Zr(s),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=s}reset(){this.mode=null,this.displayedMemory.reset(),this.nonDisplayedMemory.reset(),this.lastOutputScreen.reset(),this.outputFilter.reset(),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null}getHandler(){return this.outputFilter}setHandler(t){this.outputFilter=t}setPAC(t){this.writeScreen.setPAC(t)}setBkgData(t){this.writeScreen.setBkgData(t)}setMode(t){t!==this.mode&&(this.mode=t,this.logger.log(2,(()=>"MODE="+t)),"MODE_POP-ON"===this.mode?this.writeScreen=this.nonDisplayedMemory:(this.writeScreen=this.displayedMemory,this.writeScreen.reset()),"MODE_ROLL-UP"!==this.mode&&(this.displayedMemory.nrRollUpRows=null,this.nonDisplayedMemory.nrRollUpRows=null),this.mode=t)}insertChars(t){for(let e=0;ee+": "+this.writeScreen.getDisplayText(!0))),"MODE_PAINT-ON"!==this.mode&&"MODE_ROLL-UP"!==this.mode||(this.logger.log(1,(()=>"DISPLAYED: "+this.displayedMemory.getDisplayText(!0))),this.outputDataUpdate())}ccRCL(){this.logger.log(2,"RCL - Resume Caption Loading"),this.setMode("MODE_POP-ON")}ccBS(){this.logger.log(2,"BS - BackSpace"),"MODE_TEXT"!==this.mode&&(this.writeScreen.backSpace(),this.writeScreen===this.displayedMemory&&this.outputDataUpdate())}ccAOF(){}ccAON(){}ccDER(){this.logger.log(2,"DER- Delete to End of Row"),this.writeScreen.clearToEndOfRow(),this.outputDataUpdate()}ccRU(t){this.logger.log(2,"RU("+t+") - Roll Up"),this.writeScreen=this.displayedMemory,this.setMode("MODE_ROLL-UP"),this.writeScreen.setRollUpRows(t)}ccFON(){this.logger.log(2,"FON - Flash On"),this.writeScreen.setPen({flash:!0})}ccRDC(){this.logger.log(2,"RDC - Resume Direct Captioning"),this.setMode("MODE_PAINT-ON")}ccTR(){this.logger.log(2,"TR"),this.setMode("MODE_TEXT")}ccRTD(){this.logger.log(2,"RTD"),this.setMode("MODE_TEXT")}ccEDM(){this.logger.log(2,"EDM - Erase Displayed Memory"),this.displayedMemory.reset(),this.outputDataUpdate(!0)}ccCR(){this.logger.log(2,"CR - Carriage Return"),this.writeScreen.rollUp(),this.outputDataUpdate(!0)}ccENM(){this.logger.log(2,"ENM - Erase Non-displayed Memory"),this.nonDisplayedMemory.reset()}ccEOC(){if(this.logger.log(2,"EOC - End Of Caption"),"MODE_POP-ON"===this.mode){const t=this.displayedMemory;this.displayedMemory=this.nonDisplayedMemory,this.nonDisplayedMemory=t,this.writeScreen=this.nonDisplayedMemory,this.logger.log(1,(()=>"DISP: "+this.displayedMemory.getDisplayText()))}this.outputDataUpdate(!0)}ccTO(t){this.logger.log(2,"TO("+t+") - Tab Offset"),this.writeScreen.moveCursor(t)}ccMIDROW(t){const e={flash:!1};if(e.underline=t%2==1,e.italics=t>=46,e.italics)e.foreground="white";else{const s=Math.floor(t/2)-16,i=["white","green","blue","cyan","red","yellow","magenta"];e.foreground=i[s]}this.logger.log(2,"MIDROW: "+JSON.stringify(e)),this.writeScreen.setPen(e)}outputDataUpdate(t=!1){const e=this.logger.time;null!==e&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,e,this.lastOutputScreen),t&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:e):this.cueStartTime=e,this.lastOutputScreen.copy(this.displayedMemory))}cueSplitAtTime(t){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,t,this.displayedMemory),this.cueStartTime=t))}}class en{constructor(t,e,s){this.channels=void 0,this.currentChannel=0,this.cmdHistory={a:null,b:null},this.logger=void 0;const i=this.logger=new qr;this.channels=[null,new tn(t,e,i),new tn(t+1,s,i)]}getHandler(t){return this.channels[t].getHandler()}setHandler(t,e){this.channels[t].setHandler(e)}addData(t,e){this.logger.time=t;for(let t=0;t"["+Xr([e[t],e[t+1]])+"] -> ("+Xr([s,i])+")"));const a=this.cmdHistory;if(s>=16&&s<=31){if(rn(s,i,a)){sn(null,null,a),this.logger.log(3,(()=>"Repeated command ("+Xr([s,i])+") is dropped"));continue}sn(s,i,this.cmdHistory),r=this.parseCmd(s,i),r||(r=this.parseMidrow(s,i)),r||(r=this.parsePAC(s,i)),r||(r=this.parseBackgroundAttributes(s,i))}else sn(null,null,a);if(!r&&(n=this.parseChars(s,i),n)){const t=this.currentChannel;if(t&&t>0){this.channels[t].insertChars(n)}else this.logger.log(2,"No channel found yet. TEXT-MODE?")}r||n||this.logger.log(2,(()=>"Couldn't parse cleaned data "+Xr([s,i])+" orig: "+Xr([e[t],e[t+1]])))}}parseCmd(t,e){if(!((20===t||28===t||21===t||29===t)&&e>=32&&e<=47)&&!((23===t||31===t)&&e>=33&&e<=35))return!1;const s=20===t||21===t||23===t?1:2,i=this.channels[s];return 20===t||21===t||28===t||29===t?32===e?i.ccRCL():33===e?i.ccBS():34===e?i.ccAOF():35===e?i.ccAON():36===e?i.ccDER():37===e?i.ccRU(2):38===e?i.ccRU(3):39===e?i.ccRU(4):40===e?i.ccFON():41===e?i.ccRDC():42===e?i.ccTR():43===e?i.ccRTD():44===e?i.ccEDM():45===e?i.ccCR():46===e?i.ccENM():47===e&&i.ccEOC():i.ccTO(e-32),this.currentChannel=s,!0}parseMidrow(t,e){let s=0;if((17===t||25===t)&&e>=32&&e<=47){if(s=17===t?1:2,s!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;const i=this.channels[s];return!!i&&(i.ccMIDROW(e),this.logger.log(3,(()=>"MIDROW ("+Xr([t,e])+")")),!0)}return!1}parsePAC(t,e){let s;if(!((t>=17&&t<=23||t>=25&&t<=31)&&e>=64&&e<=127)&&!((16===t||24===t)&&e>=64&&e<=95))return!1;const i=t<=23?1:2;s=e>=64&&e<=95?1===i?Hr[t]:Yr[t]:1===i?Vr[t]:Wr[t];const r=this.channels[i];return!!r&&(r.setPAC(this.interpretPAC(s,e)),this.currentChannel=i,!0)}interpretPAC(t,e){let s;const i={color:null,italics:!1,indent:null,underline:!1,row:t};return s=e>95?e-96:e-64,i.underline=!(1&~s),s<=13?i.color=["white","green","blue","cyan","red","yellow","magenta","white"][Math.floor(s/2)]:s<=15?(i.italics=!0,i.color="white"):i.indent=4*Math.floor((s-16)/2),i}parseChars(t,e){let s,i=null,r=null;if(t>=25?(s=2,r=t-8):(s=1,r=t),r>=17&&r<=19){let t;t=17===r?e+80:18===r?e+112:e+144,this.logger.log(2,(()=>"Special char '"+$r(t)+"' in channel "+s)),i=[t]}else t>=32&&t<=127&&(i=0===e?[t]:[t,e]);return i&&this.logger.log(3,(()=>"Char codes = "+Xr(i).join(","))),i}parseBackgroundAttributes(t,e){if(!((16===t||24===t)&&e>=32&&e<=47)&&!((23===t||31===t)&&e>=45&&e<=47))return!1;let s;const i={};16===t||24===t?(s=Math.floor((e-32)/2),i.background=jr[s],e%2==1&&(i.background=i.background+"_semi")):45===e?i.background="transparent":(i.foreground="black",47===e&&(i.underline=!0));const r=t<=23?1:2;return this.channels[r].setBkgData(i),!0}reset(){for(let t=0;tt)&&(this.startTime=t),this.endTime=e,this.screen=s,this.timelineController.createCaptionsTrack(this.trackName)}reset(){this.cueRanges=[],this.startTime=null}}var an=function(){if(null!=$&&$.VTTCue)return self.VTTCue;const t=["","lr","rl"],e=["start","middle","end","left","right"];function s(t,e){if("string"!=typeof e)return!1;if(!Array.isArray(t))return!1;const s=e.toLowerCase();return!!~t.indexOf(s)&&s}function i(t){return s(e,t)}function r(t,...e){let s=1;for(;s100)throw new Error("Position must be between 0 and 100.");E=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",r({},l,{get:function(){return T},set:function(t){const e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");T=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",r({},l,{get:function(){return S},set:function(t){if(t<0||t>100)throw new Error("Size must be between 0 and 100.");S=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",r({},l,{get:function(){return L},set:function(t){const e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");L=e,this.hasBeenReset=!0}})),o.displayState=void 0}return n.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},n}();class on{decode(t,e){if(!t)return"";if("string"!=typeof t)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(t))}}function ln(t){function e(t,e,s,i){return 3600*(0|t)+60*(0|e)+(0|s)+parseFloat(i||0)}const s=t.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return s?parseFloat(s[2])>59?e(s[2],s[3],0,s[4]):e(s[1],s[2],s[3],s[4]):null}class hn{constructor(){this.values=Object.create(null)}set(t,e){this.get(t)||""===e||(this.values[t]=e)}get(t,e,s){return s?this.has(t)?this.values[t]:e[s]:this.has(t)?this.values[t]:e}has(t){return t in this.values}alt(t,e,s){for(let i=0;i=0&&s<=100)return this.set(t,s),!0}return!1}}function dn(t,e,s,i){const r=i?t.split(i):[t];for(const t in r){if("string"!=typeof r[t])continue;const i=r[t].split(s);if(2!==i.length)continue;e(i[0],i[1])}}const cn=new an(0,0,""),un="middle"===cn.align?"middle":"center";function fn(t,e,s){const i=t;function r(){const e=ln(t);if(null===e)throw new Error("Malformed timestamp: "+i);return t=t.replace(/^[^\sa-zA-Z-]+/,""),e}function n(){t=t.replace(/^\s+/,"")}if(n(),e.startTime=r(),n(),"--\x3e"!==t.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);t=t.slice(3),n(),e.endTime=r(),n(),function(t,e){const i=new hn;dn(t,(function(t,e){let r;switch(t){case"region":for(let r=s.length-1;r>=0;r--)if(s[r].id===e){i.set(t,s[r].region);break}break;case"vertical":i.alt(t,e,["rl","lr"]);break;case"line":r=e.split(","),i.integer(t,r[0]),i.percent(t,r[0])&&i.set("snapToLines",!1),i.alt(t,r[0],["auto"]),2===r.length&&i.alt("lineAlign",r[1],["start",un,"end"]);break;case"position":r=e.split(","),i.percent(t,r[0]),2===r.length&&i.alt("positionAlign",r[1],["start",un,"end","line-left","line-right","auto"]);break;case"size":i.percent(t,e);break;case"align":i.alt(t,e,["start",un,"end","left","right"])}}),/:/,/\s/),e.region=i.get("region",null),e.vertical=i.get("vertical","");let r=i.get("line","auto");"auto"===r&&-1===cn.line&&(r=-1),e.line=r,e.lineAlign=i.get("lineAlign","start"),e.snapToLines=i.get("snapToLines",!0),e.size=i.get("size",100),e.align=i.get("align",un);let n=i.get("position","auto");"auto"===n&&50===cn.position&&(n="start"===e.align||"left"===e.align?0:"end"===e.align||"right"===e.align?100:50),e.position=n}(t,e)}function gn(t){return t.replace(//gi,"\n")}class mn{constructor(){this.state="INITIAL",this.buffer="",this.decoder=new on,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}parse(t){const e=this;function s(){let t=e.buffer,s=0;for(t=gn(t);s>>0).toString()};function En(t,e,s){return yn(t.toString())+yn(e.toString())+yn(s)}function Tn(t,e,s,i,r,n,a){const o=new mn,l=vt(new Uint8Array(t)).trim().replace(pn,"\n").split("\n"),h=[],d=e?function(t,e=1){return ur(t,9e4,1/e)}(e.baseTime,e.timescale):0;let c,u="00:00.000",g=0,m=0,p=!0;o.oncue=function(t){const n=s[i];let a=s.ccOffset;const o=(g-d)/9e4;if(null!=n&&n.new&&(void 0!==m?a=s.ccOffset=n.start:function(t,e,s){let i=t[e],r=t[i.prevCC];if(!r||!r.new&&i.new)return t.ccOffset=t.presentationOffset=i.start,void(i.new=!1);for(;null!=(n=r)&&n.new;){var n;t.ccOffset+=i.start-r.start,i.new=!1,i=r,r=t[i.prevCC]}t.presentationOffset=s}(s,i,o)),o){if(!e)return void(c=new Error("Missing initPTS for VTT MPEGTS"));a=o-s.presentationOffset}const l=t.endTime-t.startTime,u=yr(9e4*(t.startTime+a-m),9e4*r)/9e4;t.startTime=Math.max(u,0),t.endTime=Math.max(u+l,0);const f=t.text.trim();t.text=decodeURIComponent(encodeURIComponent(f)),t.id||(t.id=En(t.startTime,t.endTime,f)),t.endTime>0&&h.push(t)},o.onparsingerror=function(t){c=t},o.onflush=function(){c?a(c):n(h)},l.forEach((t=>{if(p){if(vn(t,"X-TIMESTAMP-MAP=")){p=!1,t.slice(16).split(",").forEach((t=>{vn(t,"LOCAL:")?u=t.slice(6):vn(t,"MPEGTS:")&&(g=parseInt(t.slice(7)))}));try{m=function(t){let e=parseInt(t.slice(-3));const s=parseInt(t.slice(-6,-4)),i=parseInt(t.slice(-9,-7)),r=t.length>9?parseInt(t.substring(0,t.indexOf(":"))):0;if(!(f(e)&&f(s)&&f(i)&&f(r)))throw Error(`Malformed X-TIMESTAMP-MAP: Local:${t}`);return e+=1e3*s,e+=6e4*i,e+=36e5*r,e}(u)/1e3}catch(t){c=t}return}""===t&&(p=!1)}o.parse(t+"\n")})),o.flush()}const Sn="stpp.ttml.im1t",Ln=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,An=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,Rn={left:"start",center:"center",right:"end",start:"start",end:"end"};function bn(t,e,s,i){const r=_t(new Uint8Array(t),["mdat"]);if(0===r.length)return void i(new Error("Could not parse IMSC1 mdat"));const n=r.map((t=>vt(t))),a=function(t,e,s=1,i=!1){return ur(t,e,1/s,i)}(e.baseTime,1,e.timescale);try{n.forEach((t=>s(function(t,e){const s=(new DOMParser).parseFromString(t,"text/xml"),i=s.getElementsByTagName("tt")[0];if(!i)throw new Error("Invalid ttml");const r={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},n=Object.keys(r).reduce(((t,e)=>(t[e]=i.getAttribute(`ttp:${e}`)||r[e],t)),{}),a="preserve"!==i.getAttribute("xml:space"),o=wn(kn(i,"styling","style")),l=wn(kn(i,"layout","region")),h=kn(i,"body","[begin]");return[].map.call(h,(t=>{const s=Dn(t,a);if(!s||!t.hasAttribute("begin"))return null;const i=Cn(t.getAttribute("begin"),n),r=Cn(t.getAttribute("dur"),n);let h=Cn(t.getAttribute("end"),n);if(null===i)throw _n(t);if(null===h){if(null===r)throw _n(t);h=i+r}const d=new an(i-e,h-e,s);d.id=En(d.startTime,d.endTime,d.text);const c=function(t,e,s){const i="http://www.w3.org/ns/ttml#styling";let r=null;const n=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],a=null!=t&&t.hasAttribute("style")?t.getAttribute("style"):null;a&&s.hasOwnProperty(a)&&(r=s[a]);return n.reduce(((s,n)=>{const a=In(e,i,n)||In(t,i,n)||In(r,i,n);return a&&(s[n]=a),s}),{})}(l[t.getAttribute("region")],o[t.getAttribute("style")],o),{textAlign:f}=c;if(f){const t=Rn[f];t&&(d.lineAlign=t),d.align=f}return u(d,c),d})).filter((t=>null!==t))}(t,a))))}catch(t){i(t)}}function kn(t,e,s){const i=t.getElementsByTagName(e)[0];return i?[].slice.call(i.querySelectorAll(s)):[]}function wn(t){return t.reduce(((t,e)=>{const s=e.getAttribute("xml:id");return s&&(t[s]=e),t}),{})}function Dn(t,e){return[].slice.call(t.childNodes).reduce(((t,s,i)=>{var r;return"br"===s.nodeName&&i?t+"\n":null!=(r=s.childNodes)&&r.length?Dn(s,e):e?t+s.textContent.trim().replace(/\s+/g," "):t+s.textContent}),"")}function In(t,e,s){return t&&t.hasAttributeNS(e,s)?t.getAttributeNS(e,s):null}function _n(t){return new Error(`Could not parse ttml timestamp ${t}`)}function Cn(t,e){if(!t)return null;let s=ln(t);return null===s&&(Ln.test(t)?s=function(t,e){const s=Ln.exec(t),i=(0|s[4])+(0|s[5])/e.subFrameRate;return 3600*(0|s[1])+60*(0|s[2])+(0|s[3])+i/e.frameRate}(t,e):An.test(t)&&(s=function(t,e){const s=An.exec(t),i=Number(s[1]);switch(s[2]){case"h":return 3600*i;case"m":return 60*i;case"ms":return 1e3*i;case"f":return i/e.frameRate;case"t":return i/e.tickRate}return i}(t,e))),s}function xn(t){return t.characteristics&&/transcribes-spoken-dialog/gi.test(t.characteristics)&&/describes-music-and-sound/gi.test(t.characteristics)?"captions":"subtitles"}function Pn(t,e){return!!t&&t.kind===xn(e)&&Mr(e,t)}class Mn{constructor(t){this.hls=void 0,this.autoLevelCapping=void 0,this.firstLevel=void 0,this.media=void 0,this.restrictedLevels=void 0,this.timer=void 0,this.clientRect=void 0,this.streamController=void 0,this.hls=t,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.firstLevel=-1,this.media=null,this.restrictedLevels=[],this.timer=void 0,this.clientRect=null,this.registerListeners()}setStreamController(t){this.streamController=t}destroy(){this.hls&&this.unregisterListener(),this.timer&&this.stopCapping(),this.media=null,this.clientRect=null,this.hls=this.streamController=null}registerListeners(){const{hls:t}=this;t.on(p.FPS_DROP_LEVEL_CAPPING,this.onFpsDropLevelCapping,this),t.on(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(p.BUFFER_CODECS,this.onBufferCodecs,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this)}unregisterListener(){const{hls:t}=this;t.off(p.FPS_DROP_LEVEL_CAPPING,this.onFpsDropLevelCapping,this),t.off(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(p.BUFFER_CODECS,this.onBufferCodecs,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this)}onFpsDropLevelCapping(t,e){const s=this.hls.levels[e.droppedLevel];this.isLevelAllowed(s)&&this.restrictedLevels.push({bitrate:s.bitrate,height:s.height,width:s.width})}onMediaAttaching(t,e){this.media=e.media instanceof HTMLVideoElement?e.media:null,this.clientRect=null,this.timer&&this.hls.levels.length&&this.detectPlayerSize()}onManifestParsed(t,e){const s=this.hls;this.restrictedLevels=[],this.firstLevel=e.firstLevel,s.config.capLevelToPlayerSize&&e.video&&this.startCapping()}onLevelsUpdated(t,e){this.timer&&f(this.autoLevelCapping)&&this.detectPlayerSize()}onBufferCodecs(t,e){this.hls.config.capLevelToPlayerSize&&e.video&&this.startCapping()}onMediaDetaching(){this.stopCapping()}detectPlayerSize(){if(this.media){if(this.mediaHeight<=0||this.mediaWidth<=0)return void(this.clientRect=null);const t=this.hls.levels;if(t.length){const e=this.hls,s=this.getMaxLevel(t.length-1);s!==this.autoLevelCapping&&A.log(`Setting autoLevelCapping to ${s}: ${t[s].height}p@${t[s].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`),e.autoLevelCapping=s,e.autoLevelCapping>this.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}}getMaxLevel(t){const e=this.hls.levels;if(!e.length)return-1;const s=e.filter(((e,s)=>this.isLevelAllowed(e)&&s<=t));return this.clientRect=null,Mn.getMaxLevelByMediaSize(s,this.mediaWidth,this.mediaHeight)}startCapping(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())}stopCapping(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)}getDimensions(){if(this.clientRect)return this.clientRect;const t=this.media,e={width:0,height:0};if(t){const s=t.getBoundingClientRect();e.width=s.width,e.height=s.height,e.width||e.height||(e.width=s.right-s.left||t.width||0,e.height=s.bottom-s.top||t.height||0)}return this.clientRect=e,e}get mediaWidth(){return this.getDimensions().width*this.contentScaleFactor}get mediaHeight(){return this.getDimensions().height*this.contentScaleFactor}get contentScaleFactor(){let t=1;if(!this.hls.config.ignoreDevicePixelRatio)try{t=self.devicePixelRatio}catch(t){}return t}isLevelAllowed(t){return!this.restrictedLevels.some((e=>t.bitrate===e.bitrate&&t.width===e.width&&t.height===e.height))}static getMaxLevelByMediaSize(t,e,s){if(null==t||!t.length)return-1;let i=t.length-1;const r=Math.max(e,s);for(let e=0;e=r||s.height>=r)&&(n=s,!(a=t[e+1])||n.width!==a.width||n.height!==a.height)){i=e;break}}var n,a;return i}}const Fn="[eme]";class On{constructor(t){this.hls=void 0,this.config=void 0,this.media=null,this.keyFormatPromise=null,this.keySystemAccessPromises={},this._requestLicenseFailureCount=0,this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},this.setMediaKeysQueue=On.CDMCleanupPromise?[On.CDMCleanupPromise]:[],this.onMediaEncrypted=this._onMediaEncrypted.bind(this),this.onWaitingForKey=this._onWaitingForKey.bind(this),this.debug=A.debug.bind(A,Fn),this.log=A.log.bind(A,Fn),this.warn=A.warn.bind(A,Fn),this.error=A.error.bind(A,Fn),this.hls=t,this.config=t.config,this.registerListeners()}destroy(){this.unregisterListeners(),this.onMediaDetached();const t=this.config;t.requestMediaKeySystemAccessFunc=null,t.licenseXhrSetup=t.licenseResponseCallback=void 0,t.drmSystems=t.drmSystemOptions={},this.hls=this.onMediaEncrypted=this.onWaitingForKey=this.keyIdToKeySessionPromise=null,this.config=null}registerListeners(){this.hls.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(p.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.on(p.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(p.MANIFEST_LOADED,this.onManifestLoaded,this)}unregisterListeners(){this.hls.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(p.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.off(p.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(p.MANIFEST_LOADED,this.onManifestLoaded,this)}getLicenseServerUrl(t){const{drmSystems:e,widevineLicenseUrl:s}=this.config,i=e[t];if(i)return i.licenseUrl;if(t===G.WIDEVINE&&s)return s;throw new Error(`no license server URL configured for key-system "${t}"`)}getServerCertificateUrl(t){const{drmSystems:e}=this.config,s=e[t];if(s)return s.serverCertificateUrl;this.log(`No Server Certificate in config.drmSystems["${t}"]`)}attemptKeySystemAccess(t){const e=this.hls.levels,s=(t,e,s)=>!!t&&s.indexOf(t)===e,i=e.map((t=>t.audioCodec)).filter(s),r=e.map((t=>t.videoCodec)).filter(s);return i.length+r.length===0&&r.push("avc1.42e01e"),new Promise(((e,s)=>{const n=t=>{const a=t.shift();this.getMediaKeysPromise(a,i,r).then((t=>e({keySystem:a,mediaKeys:t}))).catch((e=>{t.length?n(t):s(e instanceof Nn?e:new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_NO_ACCESS,error:e,fatal:!0},e.message))}))};n(t)}))}requestMediaKeySystemAccess(t,e){const{requestMediaKeySystemAccessFunc:s}=this.config;if("function"!=typeof s){let t=`Configured requestMediaKeySystemAccess is not a function ${s}`;return null===tt&&"http:"===self.location.protocol&&(t=`navigator.requestMediaKeySystemAccess is not available over insecure protocol ${location.protocol}`),Promise.reject(new Error(t))}return s(t,e)}getMediaKeysPromise(t,e,s){const i=function(t,e,s,i){let r;switch(t){case G.FAIRPLAY:r=["cenc","sinf"];break;case G.WIDEVINE:case G.PLAYREADY:r=["cenc"];break;case G.CLEARKEY:r=["cenc","keyids"];break;default:throw new Error(`Unknown key-system: ${t}`)}return function(t,e,s,i){return[{initDataTypes:t,persistentState:i.persistentState||"optional",distinctiveIdentifier:i.distinctiveIdentifier||"optional",sessionTypes:i.sessionTypes||[i.sessionType||"temporary"],audioCapabilities:e.map((t=>({contentType:`audio/mp4; codecs="${t}"`,robustness:i.audioRobustness||"",encryptionScheme:i.audioEncryptionScheme||null}))),videoCapabilities:s.map((t=>({contentType:`video/mp4; codecs="${t}"`,robustness:i.videoRobustness||"",encryptionScheme:i.videoEncryptionScheme||null})))}]}(r,e,s,i)}(t,e,s,this.config.drmSystemOptions),r=this.keySystemAccessPromises[t];let n=null==r?void 0:r.keySystemAccess;if(!n){this.log(`Requesting encrypted media "${t}" key-system access with config: ${JSON.stringify(i)}`),n=this.requestMediaKeySystemAccess(t,i);const e=this.keySystemAccessPromises[t]={keySystemAccess:n};return n.catch((e=>{this.log(`Failed to obtain access to key-system "${t}": ${e}`)})),n.then((s=>{this.log(`Access for key-system "${s.keySystem}" obtained`);const i=this.fetchServerCertificate(t);return this.log(`Create media-keys for "${t}"`),e.mediaKeys=s.createMediaKeys().then((e=>(this.log(`Media-keys created for "${t}"`),i.then((s=>s?this.setMediaKeysServerCertificate(e,t,s):e))))),e.mediaKeys.catch((e=>{this.error(`Failed to create media-keys for "${t}"}: ${e}`)})),e.mediaKeys}))}return n.then((()=>r.mediaKeys))}createMediaKeySessionContext({decryptdata:t,keySystem:e,mediaKeys:s}){this.log(`Creating key-system session "${e}" keyId: ${Tt(t.keyId||[])}`);const i=s.createSession(),r={decryptdata:t,keySystem:e,mediaKeys:s,mediaKeysSession:i,keyStatus:"status-pending"};return this.mediaKeySessions.push(r),r}renewKeySession(t){const e=t.decryptdata;if(e.pssh){const s=this.createMediaKeySessionContext(t),i=this.getKeyIdString(e),r="cenc";this.keyIdToKeySessionPromise[i]=this.generateRequestWithPreferredKeySession(s,r,e.pssh,"expired")}else this.warn("Could not renew expired session. Missing pssh initData.");this.removeSession(t)}getKeyIdString(t){if(!t)throw new Error("Could not read keyId of undefined decryptdata");if(null===t.keyId)throw new Error("keyId is null");return Tt(t.keyId)}updateKeySession(t,e){var s;const i=t.mediaKeysSession;return this.log(`Updating key-session "${i.sessionId}" for keyID ${Tt((null==(s=t.decryptdata)?void 0:s.keyId)||[])}\n } (data length: ${e?e.byteLength:e})`),i.update(e)}selectKeySystemFormat(t){const e=Object.keys(t.levelkeys||{});return this.keyFormatPromise||(this.log(`Selecting key-system from fragment (sn: ${t.sn} ${t.type}: ${t.level}) key formats ${e.join(", ")}`),this.keyFormatPromise=this.getKeyFormatPromise(e)),this.keyFormatPromise}getKeyFormatPromise(t){return new Promise(((e,s)=>{const i=Z(this.config),r=t.map(W).filter((t=>!!t&&-1!==i.indexOf(t)));return this.getKeySystemSelectionPromise(r).then((({keySystem:t})=>{const i=J(t);i?e(i):s(new Error(`Unable to find format for key-system "${t}"`))})).catch(s)}))}loadKey(t){const e=t.keyInfo.decryptdata,s=this.getKeyIdString(e),i=`(keyId: ${s} format: "${e.keyFormat}" method: ${e.method} uri: ${e.uri})`;this.log(`Starting session for key ${i}`);let r=this.keyIdToKeySessionPromise[s];return r||(r=this.keyIdToKeySessionPromise[s]=this.getKeySystemForKeyPromise(e).then((({keySystem:s,mediaKeys:r})=>(this.throwIfDestroyed(),this.log(`Handle encrypted media sn: ${t.frag.sn} ${t.frag.type}: ${t.frag.level} using key ${i}`),this.attemptSetMediaKeys(s,r).then((()=>{this.throwIfDestroyed();const t=this.createMediaKeySessionContext({keySystem:s,mediaKeys:r,decryptdata:e});return this.generateRequestWithPreferredKeySession(t,"cenc",e.pssh,"playlist-key")}))))),r.catch((t=>this.handleError(t)))),r}throwIfDestroyed(t="Invalid state"){if(!this.hls)throw new Error("invalid state")}handleError(t){this.hls&&(this.error(t.message),t instanceof Nn?this.hls.trigger(p.ERROR,t.data):this.hls.trigger(p.ERROR,{type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_NO_KEYS,error:t,fatal:!0}))}getKeySystemForKeyPromise(t){const e=this.getKeyIdString(t),s=this.keyIdToKeySessionPromise[e];if(!s){const e=W(t.keyFormat),s=e?[e]:Z(this.config);return this.attemptKeySystemAccess(s)}return s}getKeySystemSelectionPromise(t){if(t.length||(t=Z(this.config)),0===t.length)throw new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_NO_CONFIGURED_LICENSE,fatal:!0},`Missing key-system license configuration options ${JSON.stringify({drmSystems:this.config.drmSystems})}`);return this.attemptKeySystemAccess(t)}_onMediaEncrypted(t){const{initDataType:e,initData:s}=t,i=`"${t.type}" event: init data type: "${e}"`;if(this.debug(i),null===s)return;let r,n;if("sinf"===e&&this.config.drmSystems[G.FAIRPLAY]){const t=Rt(new Uint8Array(s));try{const e=N(JSON.parse(t).sinf),s=Nt(new Uint8Array(e));if(!s)throw new Error("'schm' box missing or not cbcs/cenc with schi > tenc");r=s.subarray(8,24),n=G.FAIRPLAY}catch(t){return void this.warn(`${i} Failed to parse sinf: ${t}`)}}else{const t=function(t){const e=[];if(t instanceof ArrayBuffer){const s=t.byteLength;let i=0;for(;i+32t.systemId===z))[0];if(!e)return void(0===t.length||t.some((t=>!t.systemId))?this.warn(`${i} contains incomplete or invalid pssh data`):this.log(`ignoring ${i} for ${t.map((t=>Q(t.systemId))).join(",")} pssh data in favor of playlist keys`));if(n=Q(e.systemId),0===e.version&&e.data){const t=e.data.length-22;r=e.data.subarray(t,t+16)}}if(!n||!r)return;const a=Tt(r),{keyIdToKeySessionPromise:o,mediaKeySessions:l}=this;let h=o[a];for(let t=0;tthis.generateRequestWithPreferredKeySession(i,e,s,"encrypted-event-key-match")));break}}h||(h=o[a]=this.getKeySystemSelectionPromise([n]).then((({keySystem:t,mediaKeys:i})=>{var n;this.throwIfDestroyed();const o=new jt("ISO-23001-7",a,null!=(n=J(t))?n:"");return o.pssh=new Uint8Array(s),o.keyId=r,this.attemptSetMediaKeys(t,i).then((()=>{this.throwIfDestroyed();const r=this.createMediaKeySessionContext({decryptdata:o,keySystem:t,mediaKeys:i});return this.generateRequestWithPreferredKeySession(r,e,s,"encrypted-event-no-match")}))}))),h.catch((t=>this.handleError(t)))}_onWaitingForKey(t){this.log(`"${t.type}" event`)}attemptSetMediaKeys(t,e){const s=this.setMediaKeysQueue.slice();this.log(`Setting media-keys for "${t}"`);const i=Promise.all(s).then((()=>{if(!this.media)throw new Error("Attempted to set mediaKeys without media element attached");return this.media.setMediaKeys(e)}));return this.setMediaKeysQueue.push(i),i.then((()=>{this.log(`Media-keys set for "${t}"`),s.push(i),this.setMediaKeysQueue=this.setMediaKeysQueue.filter((t=>-1===s.indexOf(t)))}))}generateRequestWithPreferredKeySession(t,e,s,i){var r,n;const a=null==(r=this.config.drmSystems)||null==(n=r[t.keySystem])?void 0:n.generateRequest;if(a)try{const i=a.call(this.hls,e,s,t);if(!i)throw new Error("Invalid response from configured generateRequest filter");e=i.initDataType,s=t.decryptdata.pssh=i.initData?new Uint8Array(i.initData):null}catch(t){var o;if(this.warn(t.message),null!=(o=this.hls)&&o.config.debug)throw t}if(null===s)return this.log(`Skipping key-session request for "${i}" (no initData)`),Promise.resolve(t);const l=this.getKeyIdString(t.decryptdata);this.log(`Generating key-session request for "${i}": ${l} (init data type: ${e} length: ${s?s.byteLength:null})`);const h=new _r,d=t._onmessage=e=>{const s=t.mediaKeysSession;if(!s)return void h.emit("error",new Error("invalid state"));const{messageType:i,message:r}=e;this.log(`"${i}" message event for session "${s.sessionId}" message size: ${r.byteLength}`),"license-request"===i||"license-renewal"===i?this.renewLicense(t,r).catch((t=>{this.handleError(t),h.emit("error",t)})):"license-release"===i?t.keySystem===G.FAIRPLAY&&(this.updateKeySession(t,B("acknowledged")),this.removeSession(t)):this.warn(`unhandled media key message type "${i}"`)},c=t._onkeystatuseschange=e=>{if(!t.mediaKeysSession)return void h.emit("error",new Error("invalid state"));this.onKeyStatusChange(t);const s=t.keyStatus;h.emit("keyStatus",s),"expired"===s&&(this.warn(`${t.keySystem} expired for key ${l}`),this.renewKeySession(t))};t.mediaKeysSession.addEventListener("message",d),t.mediaKeysSession.addEventListener("keystatuseschange",c);const u=new Promise(((t,e)=>{h.on("error",e),h.on("keyStatus",(s=>{s.startsWith("usable")?t():"output-restricted"===s?e(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED,fatal:!1},"HDCP level output restricted")):"internal-error"===s?e(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_STATUS_INTERNAL_ERROR,fatal:!0},`key status changed to "${s}"`)):"expired"===s?e(new Error("key expired while generating request")):this.warn(`unhandled key status change "${s}"`)}))}));return t.mediaKeysSession.generateRequest(e,s).then((()=>{var e;this.log(`Request generated for key-session "${null==(e=t.mediaKeysSession)?void 0:e.sessionId}" keyId: ${l}`)})).catch((t=>{throw new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_NO_SESSION,error:t,fatal:!1},`Error generating key-session request: ${t}`)})).then((()=>u)).catch((e=>{throw h.removeAllListeners(),this.removeSession(t),e})).then((()=>(h.removeAllListeners(),t)))}onKeyStatusChange(t){t.mediaKeysSession.keyStatuses.forEach(((e,s)=>{this.log(`key status change "${e}" for keyStatuses keyId: ${Tt("buffer"in s?new Uint8Array(s.buffer,s.byteOffset,s.byteLength):new Uint8Array(s))} session keyId: ${Tt(new Uint8Array(t.decryptdata.keyId||[]))} uri: ${t.decryptdata.uri}`),t.keyStatus=e}))}fetchServerCertificate(t){const e=this.config,s=new(0,e.loader)(e),i=this.getServerCertificateUrl(t);return i?(this.log(`Fetching server certificate for "${t}"`),new Promise(((r,n)=>{const a={responseType:"arraybuffer",url:i},o=e.certLoadPolicy.default,l={loadPolicy:o,timeout:o.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0},d={onSuccess:(t,e,s,i)=>{r(t.data)},onError:(e,s,r,o)=>{n(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,fatal:!0,networkDetails:r,response:h({url:a.url,data:void 0},e)},`"${t}" certificate request failed (${i}). Status: ${e.code} (${e.text})`))},onTimeout:(e,s,r)=>{n(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED,fatal:!0,networkDetails:r,response:{url:a.url,data:void 0}},`"${t}" certificate request timed out (${i})`))},onAbort:(t,e,s)=>{n(new Error("aborted"))}};s.load(a,l,d)}))):Promise.resolve()}setMediaKeysServerCertificate(t,e,s){return new Promise(((i,r)=>{t.setServerCertificate(s).then((r=>{this.log(`setServerCertificate ${r?"success":"not supported by CDM"} (${null==s?void 0:s.byteLength}) on "${e}"`),i(t)})).catch((t=>{r(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED,error:t,fatal:!0},t.message))}))}))}renewLicense(t,e){return this.requestLicense(t,new Uint8Array(e)).then((e=>this.updateKeySession(t,new Uint8Array(e)).catch((t=>{throw new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_SESSION_UPDATE_FAILED,error:t,fatal:!0},t.message)}))))}unpackPlayReadyKeyMessage(t,e){const s=String.fromCharCode.apply(null,new Uint16Array(e.buffer));if(!s.includes("PlayReadyKeyMessage"))return t.setRequestHeader("Content-Type","text/xml; charset=utf-8"),e;const i=(new DOMParser).parseFromString(s,"application/xml"),r=i.querySelectorAll("HttpHeader");if(r.length>0){let e;for(let s=0,i=r.length;s in key message");return B(atob(l))}setupLicenseXHR(t,e,s,i){const r=this.config.licenseXhrSetup;return r?Promise.resolve().then((()=>{if(!s.decryptdata)throw new Error("Key removed");return r.call(this.hls,t,e,s,i)})).catch((n=>{if(!s.decryptdata)throw n;return t.open("POST",e,!0),r.call(this.hls,t,e,s,i)})).then((s=>{t.readyState||t.open("POST",e,!0);return{xhr:t,licenseChallenge:s||i}})):(t.open("POST",e,!0),Promise.resolve({xhr:t,licenseChallenge:i}))}requestLicense(t,e){const s=this.config.keyLoadPolicy.default;return new Promise(((i,r)=>{const n=this.getLicenseServerUrl(t.keySystem);this.log(`Sending license request to URL: ${n}`);const a=new XMLHttpRequest;a.responseType="arraybuffer",a.onreadystatechange=()=>{if(!this.hls||!t.mediaKeysSession)return r(new Error("invalid state"));if(4===a.readyState)if(200===a.status){this._requestLicenseFailureCount=0;let e=a.response;this.log(`License received ${e instanceof ArrayBuffer?e.byteLength:e}`);const s=this.config.licenseResponseCallback;if(s)try{e=s.call(this.hls,a,n,t)}catch(t){this.error(t)}i(e)}else{const o=s.errorRetry,l=o?o.maxNumRetry:0;if(this._requestLicenseFailureCount++,this._requestLicenseFailureCount>l||a.status>=400&&a.status<500)r(new Nn({type:v.KEY_SYSTEM_ERROR,details:y.KEY_SYSTEM_LICENSE_REQUEST_FAILED,fatal:!0,networkDetails:a,response:{url:n,data:void 0,code:a.status,text:a.statusText}},`License Request XHR failed (${n}). Status: ${a.status} (${a.statusText})`));else{const s=l-this._requestLicenseFailureCount+1;this.warn(`Retrying license request, ${s} attempts left`),this.requestLicense(t,e).then(i,r)}}},t.licenseXhr&&t.licenseXhr.readyState!==XMLHttpRequest.DONE&&t.licenseXhr.abort(),t.licenseXhr=a,this.setupLicenseXHR(a,n,t,e).then((({xhr:e,licenseChallenge:s})=>{t.keySystem==G.PLAYREADY&&(s=this.unpackPlayReadyKeyMessage(e,s)),e.send(s)}))}))}onMediaAttached(t,e){if(!this.config.emeEnabled)return;const s=e.media;this.media=s,s.addEventListener("encrypted",this.onMediaEncrypted),s.addEventListener("waitingforkey",this.onWaitingForKey)}onMediaDetached(){const t=this.media,e=this.mediaKeySessions;t&&(t.removeEventListener("encrypted",this.onMediaEncrypted),t.removeEventListener("waitingforkey",this.onWaitingForKey),this.media=null),this._requestLicenseFailureCount=0,this.setMediaKeysQueue=[],this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},jt.clearKeyUriToKeyIdMap();const s=e.length;On.CDMCleanupPromise=Promise.all(e.map((t=>this.removeSession(t))).concat(null==t?void 0:t.setMediaKeys(null).catch((t=>{this.log(`Could not clear media keys: ${t}`)})))).then((()=>{s&&(this.log("finished closing key sessions and clearing media keys"),e.length=0)})).catch((t=>{this.log(`Could not close sessions and clear media keys: ${t}`)}))}onManifestLoading(){this.keyFormatPromise=null}onManifestLoaded(t,{sessionKeys:e}){if(e&&this.config.emeEnabled&&!this.keyFormatPromise){const t=e.reduce(((t,e)=>(-1===t.indexOf(e.keyFormat)&&t.push(e.keyFormat),t)),[]);this.log(`Selecting key-system from session-keys ${t.join(", ")}`),this.keyFormatPromise=this.getKeyFormatPromise(t)}}removeSession(t){const{mediaKeysSession:e,licenseXhr:s}=t;if(e){this.log(`Remove licenses and keys and close session ${e.sessionId}`),t._onmessage&&(e.removeEventListener("message",t._onmessage),t._onmessage=void 0),t._onkeystatuseschange&&(e.removeEventListener("keystatuseschange",t._onkeystatuseschange),t._onkeystatuseschange=void 0),s&&s.readyState!==XMLHttpRequest.DONE&&s.abort(),t.mediaKeysSession=t.decryptdata=t.licenseXhr=void 0;const i=this.mediaKeySessions.indexOf(t);return i>-1&&this.mediaKeySessions.splice(i,1),e.remove().catch((t=>{this.log(`Could not remove session: ${t}`)})).then((()=>e.close())).catch((t=>{this.log(`Could not close session: ${t}`)}))}}}On.CDMCleanupPromise=void 0;class Nn extends Error{constructor(t,e){super(e),this.data=void 0,t.error||(t.error=new Error(e)),this.data=t,t.err=t.error}}var Un,Bn,$n;!function(t){t.MANIFEST="m",t.AUDIO="a",t.VIDEO="v",t.MUXED="av",t.INIT="i",t.CAPTION="c",t.TIMED_TEXT="tt",t.KEY="k",t.OTHER="o"}(Un||(Un={})),function(t){t.DASH="d",t.HLS="h",t.SMOOTH="s",t.OTHER="o"}(Bn||(Bn={})),function(t){t.OBJECT="CMCD-Object",t.REQUEST="CMCD-Request",t.SESSION="CMCD-Session",t.STATUS="CMCD-Status"}($n||($n={}));const Gn={[$n.OBJECT]:["br","d","ot","tb"],[$n.REQUEST]:["bl","dl","mtp","nor","nrr","su"],[$n.SESSION]:["cid","pr","sf","sid","st","v"],[$n.STATUS]:["bs","rtp"]};class Kn{constructor(t,e){this.value=void 0,this.params=void 0,Array.isArray(t)&&(t=t.map((t=>t instanceof Kn?t:new Kn(t)))),this.value=t,this.params=e}}class Hn{constructor(t){this.description=void 0,this.description=t}}function Vn(t,e,s,i){return new Error(`failed to ${t} "${r=e,Array.isArray(r)?JSON.stringify(r):r instanceof Map?"Map{}":r instanceof Set?"Set{}":"object"==typeof r?JSON.stringify(r):String(r)}" as ${s}`,{cause:i});var r}const Yn="Bare Item";const Wn=/[\x00-\x1f\x7f]+/;function jn(t,e,s){return Vn("serialize",t,e,s)}function qn(t){if(!1===ArrayBuffer.isView(t))throw jn(t,"Byte Sequence");return`:${e=t,btoa(String.fromCharCode(...e))}:`;var e}function Xn(t){if(function(t){return t<-999999999999999||99999999999999912)throw jn(t,"Decimal");const s=e.toString();return s.includes(".")?s:`${s}.0`}function Jn(t){const e=(s=t).description||s.toString().slice(7,-1);var s;if(!1===/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(e))throw jn(e,"Token");return e}function Zn(t){switch(typeof t){case"number":if(!f(t))throw jn(t,Yn);return Number.isInteger(t)?Xn(t):Qn(t);case"string":return function(t){if(Wn.test(t))throw jn(t,"String");return`"${t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}(t);case"symbol":return Jn(t);case"boolean":return function(t){if("boolean"!=typeof t)throw jn(t,"Boolean");return t?"?1":"?0"}(t);case"object":if(t instanceof Date)return function(t){return`@${Xn(t.getTime()/1e3)}`}(t);if(t instanceof Uint8Array)return qn(t);if(t instanceof Hn)return Jn(t);default:throw jn(t,Yn)}}function ta(t){if(!1===/^[a-z*][a-z0-9\-_.*]*$/.test(t))throw jn(t,"Key");return t}function ea(t){return null==t?"":Object.entries(t).map((([t,e])=>!0===e?`;${ta(t)}`:`;${ta(t)}=${Zn(e)}`)).join("")}function sa(t){return t instanceof Kn?`${Zn(t.value)}${ea(t.params)}`:Zn(t)}function ia(t,e={whitespace:!0}){if("object"!=typeof t)throw jn(t,"Dict");const s=t instanceof Map?t.entries():Object.entries(t),i=null!=e&&e.whitespace?" ":"";return Array.from(s).map((([t,e])=>{e instanceof Kn==!1&&(e=new Kn(e));let s=ta(t);var i;return!0===e.value?s+=ea(e.params):(s+="=",Array.isArray(e.value)?s+=`(${(i=e).value.map(sa).join(" ")})${ea(i.params)}`:s+=sa(e)),s})).join(`,${i}`)}const ra=t=>Math.round(t),na=t=>100*ra(t/100),aa={br:ra,d:ra,bl:na,dl:na,mtp:na,nor:(t,e)=>(null!=e&&e.baseUrl&&(t=function(t,e){const s=new URL(t),i=new URL(e);if(s.origin!==i.origin)return t;const r=s.pathname.split("/").slice(1),n=i.pathname.split("/").slice(1,-1);for(;r[0]===n[0];)r.shift(),n.shift();for(;n.length;)n.shift(),r.unshift("..");return r.join("/")}(t,e.baseUrl)),encodeURIComponent(t)),rtp:na,tb:ra};function oa(t,e){const s={};if(null==t||"object"!=typeof t)return s;const i=Object.keys(t).sort(),r=u({},aa,null==e?void 0:e.formatters),n=null==e?void 0:e.filter;return i.forEach((i=>{if(null!=n&&n(i))return;let a=t[i];const o=r[i];o&&(a=o(a,e)),"v"===i&&1===a||"pr"==i&&1===a||(t=>"number"==typeof t?f(t):null!=t&&""!==t&&!1!==t)(a)&&((t=>"ot"===t||"sf"===t||"st"===t)(i)&&"string"==typeof a&&(a=new Hn(a)),s[i]=a)})),s}function la(t,e={}){return t?function(t,e){return ia(t,e)}(oa(t,e),u({whitespace:!1},e)):""}function ha(t,e,s){return u(t,function(t,e={}){if(!t)return{};const s=Object.entries(t),i=Object.entries(Gn).concat(Object.entries((null==e?void 0:e.customHeaderMap)||{})),r=s.reduce(((t,e)=>{var s;const[r,n]=e,a=(null==(s=i.find((t=>t[1].includes(r))))?void 0:s[0])||$n.REQUEST;return null!=t[a]||(t[a]={}),t[a][r]=n,t}),{});return Object.entries(r).reduce(((t,[s,i])=>(t[s]=la(i,e),t)),{})}(e,s))}const da=/CMCD=[^&#]+/;function ca(t,e,s){const i=function(t,e={}){if(!t)return"";const s=la(t,e);return`CMCD=${encodeURIComponent(s)}`}(e,s);if(!i)return t;if(da.test(t))return t.replace(da,i);const r=t.includes("?")?"&":"?";return`${t}${r}${i}`}function ua(t,e,s,i){t&&Object.keys(e).forEach((r=>{const n=t.filter((t=>t.groupId===r)).map((t=>{const n=u({},t);return n.details=void 0,n.attrs=new k(n.attrs),n.url=n.attrs.URI=fa(t.url,t.attrs["STABLE-RENDITION-ID"],"PER-RENDITION-URIS",s),n.groupId=n.attrs["GROUP-ID"]=e[r],n.attrs["PATHWAY-ID"]=i,n}));t.push(...n)}))}function fa(t,e,s,i){const{HOST:r,PARAMS:n,[s]:a}=i;let o;e&&(o=null==a?void 0:a[e],o&&(t=o));const l=new self.URL(t);return r&&!o&&(l.host=r),n&&Object.keys(n).sort().forEach((t=>{t&&l.searchParams.set(t,n[t])})),l.href}const ga=/^age:\s*[\d.]+\s*$/im;class ma{constructor(t){this.xhrSetup=void 0,this.requestTimeout=void 0,this.retryTimeout=void 0,this.retryDelay=void 0,this.config=null,this.callbacks=null,this.context=null,this.loader=null,this.stats=void 0,this.xhrSetup=t&&t.xhrSetup||null,this.stats=new I,this.retryDelay=0}destroy(){this.callbacks=null,this.abortInternal(),this.loader=null,this.config=null,this.context=null,this.xhrSetup=null}abortInternal(){const t=this.loader;self.clearTimeout(this.requestTimeout),self.clearTimeout(this.retryTimeout),t&&(t.onreadystatechange=null,t.onprogress=null,4!==t.readyState&&(this.stats.aborted=!0,t.abort()))}abort(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.loader)}load(t,e,s){if(this.stats.loading.start)throw new Error("Loader can only be used once.");this.stats.loading.start=self.performance.now(),this.context=t,this.config=e,this.callbacks=s,this.loadInternal()}loadInternal(){const{config:t,context:e}=this;if(!t||!e)return;const s=this.loader=new self.XMLHttpRequest,i=this.stats;i.loading.first=0,i.loaded=0,i.aborted=!1;const r=this.xhrSetup;r?Promise.resolve().then((()=>{if(this.loader===s&&!this.stats.aborted)return r(s,e.url)})).catch((t=>{if(this.loader===s&&!this.stats.aborted)return s.open("GET",e.url,!0),r(s,e.url)})).then((()=>{this.loader!==s||this.stats.aborted||this.openAndSendXhr(s,e,t)})).catch((t=>{this.callbacks.onError({code:s.status,text:t.message},e,s,i)})):this.openAndSendXhr(s,e,t)}openAndSendXhr(t,e,s){t.readyState||t.open("GET",e.url,!0);const i=e.headers,{maxTimeToFirstByteMs:r,maxLoadTimeMs:n}=s.loadPolicy;if(i)for(const e in i)t.setRequestHeader(e,i[e]);e.rangeEnd&&t.setRequestHeader("Range","bytes="+e.rangeStart+"-"+(e.rangeEnd-1)),t.onreadystatechange=this.readystatechange.bind(this),t.onprogress=this.loadprogress.bind(this),t.responseType=e.responseType,self.clearTimeout(this.requestTimeout),s.timeout=r&&f(r)?r:n,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),s.timeout),t.send()}readystatechange(){const{context:t,loader:e,stats:s}=this;if(!t||!e)return;const i=e.readyState,r=this.config;if(!s.aborted&&i>=2&&(0===s.loading.first&&(s.loading.first=Math.max(self.performance.now(),s.loading.start),r.timeout!==r.loadPolicy.maxLoadTimeMs&&(self.clearTimeout(this.requestTimeout),r.timeout=r.loadPolicy.maxLoadTimeMs,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),r.loadPolicy.maxLoadTimeMs-(s.loading.first-s.loading.start)))),4===i)){self.clearTimeout(this.requestTimeout),e.onreadystatechange=null,e.onprogress=null;const i=e.status,n="text"!==e.responseType;if(i>=200&&i<300&&(n&&e.response||null!==e.responseText)){s.loading.end=Math.max(self.performance.now(),s.loading.first);const r=n?e.response:e.responseText,a="arraybuffer"===e.responseType?r.byteLength:r.length;if(s.loaded=s.total=a,s.bwEstimate=8e3*s.total/(s.loading.end-s.loading.first),!this.callbacks)return;const o=this.callbacks.onProgress;if(o&&o(s,t,r,e),!this.callbacks)return;const l={url:e.responseURL,data:r,code:i};this.callbacks.onSuccess(l,s,t,e)}else{const n=r.loadPolicy.errorRetry;ms(n,s.retry,!1,{url:t.url,data:void 0,code:i})?this.retry(n):(A.error(`${i} while loading ${t.url}`),this.callbacks.onError({code:i,text:e.statusText},t,e,s))}}}loadtimeout(){if(!this.config)return;const t=this.config.loadPolicy.timeoutRetry;if(ms(t,this.stats.retry,!0))this.retry(t);else{var e;A.warn(`timeout while loading ${null==(e=this.context)?void 0:e.url}`);const t=this.callbacks;t&&(this.abortInternal(),t.onTimeout(this.stats,this.context,this.loader))}}retry(t){const{context:e,stats:s}=this;this.retryDelay=fs(t,s.retry),s.retry++,A.warn(`${status?"HTTP Status "+status:"Timeout"} while loading ${null==e?void 0:e.url}, retrying ${s.retry}/${t.maxNumRetry} in ${this.retryDelay}ms`),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)}loadprogress(t){const e=this.stats;e.loaded=t.loaded,t.lengthComputable&&(e.total=t.total)}getCacheAge(){let t=null;if(this.loader&&ga.test(this.loader.getAllResponseHeaders())){const e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t}getResponseHeader(t){return this.loader&&new RegExp(`^${t}:\\s*[\\d.]+\\s*$`,"im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(t):null}}const pa=/(\d+)-(\d+)\/(\d+)/;class va{constructor(t){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=null,this.response=null,this.controller=void 0,this.context=null,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=t.fetchSetup||ya,this.controller=new self.AbortController,this.stats=new I}destroy(){this.loader=this.callbacks=this.context=this.config=this.request=null,this.abortInternal(),this.response=null,this.fetchSetup=this.controller=this.stats=null}abortInternal(){this.controller&&!this.stats.loading.end&&(this.stats.aborted=!0,this.controller.abort())}abort(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)}load(t,e,s){const i=this.stats;if(i.loading.start)throw new Error("Loader can only be used once.");i.loading.start=self.performance.now();const r=function(t,e){const s={method:"GET",mode:"cors",credentials:"same-origin",signal:e,headers:new self.Headers(u({},t.headers))};t.rangeEnd&&s.headers.set("Range","bytes="+t.rangeStart+"-"+String(t.rangeEnd-1));return s}(t,this.controller.signal),n=s.onProgress,a="arraybuffer"===t.responseType,o=a?"byteLength":"length",{maxTimeToFirstByteMs:l,maxLoadTimeMs:h}=e.loadPolicy;this.context=t,this.config=e,this.callbacks=s,this.request=this.fetchSetup(t,r),self.clearTimeout(this.requestTimeout),e.timeout=l&&f(l)?l:h,this.requestTimeout=self.setTimeout((()=>{this.abortInternal(),s.onTimeout(i,t,this.response)}),e.timeout),self.fetch(this.request).then((r=>{this.response=this.loader=r;const o=Math.max(self.performance.now(),i.loading.start);if(self.clearTimeout(this.requestTimeout),e.timeout=h,this.requestTimeout=self.setTimeout((()=>{this.abortInternal(),s.onTimeout(i,t,this.response)}),h-(o-i.loading.start)),!r.ok){const{status:t,statusText:e}=r;throw new Ea(e||"fetch, bad network response",t,r)}return i.loading.first=o,i.total=function(t){const e=t.get("Content-Range");if(e){const t=function(t){const e=pa.exec(t);if(e)return parseInt(e[2])-parseInt(e[1])+1}(e);if(f(t))return t}const s=t.get("Content-Length");if(s)return parseInt(s)}(r.headers)||i.total,n&&f(e.highWaterMark)?this.loadProgressively(r,i,t,e.highWaterMark,n):a?r.arrayBuffer():"json"===t.responseType?r.json():r.text()})).then((r=>{const a=this.response;if(!a)throw new Error("loader destroyed");self.clearTimeout(this.requestTimeout),i.loading.end=Math.max(self.performance.now(),i.loading.first);const l=r[o];l&&(i.loaded=i.total=l);const h={url:a.url,data:r,code:a.status};n&&!f(e.highWaterMark)&&n(i,t,r,a),s.onSuccess(h,i,t,a)})).catch((e=>{if(self.clearTimeout(this.requestTimeout),i.aborted)return;const r=e&&e.code||0,n=e?e.message:null;s.onError({code:r,text:n},t,e?e.details:null,i)}))}getCacheAge(){let t=null;if(this.response){const e=this.response.headers.get("age");t=e?parseFloat(e):null}return t}getResponseHeader(t){return this.response?this.response.headers.get(t):null}loadProgressively(t,e,s,i=0,r){const n=new bi,a=t.body.getReader(),o=()=>a.read().then((a=>{if(a.done)return n.dataLength&&r(e,s,n.flush(),t),Promise.resolve(new ArrayBuffer(0));const l=a.value,h=l.length;return e.loaded+=h,h=i&&r(e,s,n.flush(),t)):r(e,s,l,t),o()})).catch((()=>Promise.reject()));return o()}}function ya(t,e){return new self.Request(t.url,e)}class Ea extends Error{constructor(t,e,s){super(t),this.code=void 0,this.details=void 0,this.code=e,this.details=s}}const Ta=/\s/,Sa={newCue(t,e,s,i){const r=[];let n,a,o,l,h;const d=self.VTTCue||self.TextTrackCue;for(let u=0;u=16?l--:l++;const i=gn(h.trim()),f=En(e,s,i);null!=t&&null!=(c=t.cues)&&c.getCueById(f)||(a=new d(e,s,i),a.id=f,a.line=u+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*l/32)),r.push(a))}return t&&r.length&&(r.sort(((t,e)=>"auto"===t.line||"auto"===e.line?0:t.line>8&&e.line>8?e.line-t.line:t.line-e.line)),r.forEach((e=>Fe(t,e)))),r}},La=h(h({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,preferManagedMediaSource:!0,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,frontBufferFlushThreshold:1/0,maxBufferSize:6e7,maxBufferHole:.1,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,maxFragLookUpTolerance:.25,liveSyncDurationCount:3,liveMaxLatencyDurationCount:1/0,liveSyncDuration:void 0,liveMaxLatencyDuration:void 0,maxLiveSyncPlaybackRate:1,liveDurationInfinity:!1,liveBackBufferLength:null,maxMaxBufferLength:600,enableWorker:!0,workerPath:null,enableSoftwareAES:!0,startLevel:void 0,startFragPrefetch:!1,fpsDroppedMonitoringPeriod:5e3,fpsDroppedMonitoringThreshold:.2,appendErrorMaxRetry:3,loader:ma,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:class{constructor(t){this.hls=void 0,this.lastLevelLoadSec=0,this.lastLoadedFragLevel=-1,this.firstSelection=-1,this._nextAutoLevel=-1,this.nextAutoLevelKey="",this.audioTracksByGroup=null,this.codecTiers=null,this.timer=-1,this.fragCurrent=null,this.partCurrent=null,this.bitrateTestDelay=0,this.bwEstimator=void 0,this._abandonRulesCheck=()=>{const{fragCurrent:t,partCurrent:e,hls:s}=this,{autoLevelEnabled:i,media:r}=s;if(!t||!r)return;const n=performance.now(),a=e?e.stats:t.stats,o=e?e.duration:t.duration,l=n-a.loading.start,h=s.minAutoLevel;if(a.aborted||a.loaded&&a.loaded===a.total||t.level<=h)return this.clearTimer(),void(this._nextAutoLevel=-1);if(!i||r.paused||!r.playbackRate||!r.readyState)return;const d=s.mainForwardBufferInfo;if(null===d)return;const c=this.bwEstimator.getEstimateTTFB(),u=Math.abs(r.playbackRate);if(l<=Math.max(c,o/(2*u)*1e3))return;const g=d.len/u,m=a.loading.first?a.loading.first-a.loading.start:-1,v=a.loaded&&m>-1,y=this.getBwEstimate(),E=s.levels,T=E[t.level],S=a.total||Math.max(a.loaded,Math.round(o*T.averageBitrate/8));let L=v?l-m:l;L<1&&v&&(L=Math.min(l,8*a.loaded/y));const R=v?1e3*a.loaded/L:0,b=R?(S-a.loaded)/R:8*S/y+c/1e3;if(b<=g)return;const k=R?8*R:y;let w,D=Number.POSITIVE_INFINITY;for(w=t.level-1;w>h;w--){const t=E[w].maxBitrate;if(D=this.getTimeToLoadFrag(c/1e3,k,o*t,!E[w].details),D=b)return;if(D>10*o)return;s.nextLoadLevel=s.nextAutoLevel=w,v?this.bwEstimator.sample(l-Math.min(c,m),a.loaded):this.bwEstimator.sampleTTFB(l);const I=E[w].maxBitrate;this.getBwEstimate()*this.hls.config.abrBandWidthUpFactor>I&&this.resetEstimator(I),this.clearTimer(),A.warn(`[abr] Fragment ${t.sn}${e?" part "+e.index:""} of level ${t.level} is loading too slowly;\n Time to underbuffer: ${g.toFixed(3)} s\n Estimated load time for current fragment: ${b.toFixed(3)} s\n Estimated load time for down switch fragment: ${D.toFixed(3)} s\n TTFB estimate: ${0|m} ms\n Current BW estimate: ${f(y)?0|y:"Unknown"} bps\n New BW estimate: ${0|this.getBwEstimate()} bps\n Switching to level ${w} @ ${0|I} bps`),s.trigger(p.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:e,stats:a})},this.hls=t,this.bwEstimator=this.initEstimator(),this.registerListeners()}resetEstimator(t){t&&(A.log(`setting initial bwe to ${t}`),this.hls.config.abrEwmaDefaultEstimate=t),this.firstSelection=-1,this.bwEstimator=this.initEstimator()}initEstimator(){const t=this.hls.config;return new Is(t.abrEwmaSlowVoD,t.abrEwmaFastVoD,t.abrEwmaDefaultEstimate)}registerListeners(){const{hls:t}=this;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.FRAG_LOADING,this.onFragLoading,this),t.on(p.FRAG_LOADED,this.onFragLoaded,this),t.on(p.FRAG_BUFFERED,this.onFragBuffered,this),t.on(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(p.LEVEL_LOADED,this.onLevelLoaded,this),t.on(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(p.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.on(p.ERROR,this.onError,this)}unregisterListeners(){const{hls:t}=this;t&&(t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.FRAG_LOADING,this.onFragLoading,this),t.off(p.FRAG_LOADED,this.onFragLoaded,this),t.off(p.FRAG_BUFFERED,this.onFragBuffered,this),t.off(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(p.LEVEL_LOADED,this.onLevelLoaded,this),t.off(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(p.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.off(p.ERROR,this.onError,this))}destroy(){this.unregisterListeners(),this.clearTimer(),this.hls=this._abandonRulesCheck=null,this.fragCurrent=this.partCurrent=null}onManifestLoading(t,e){this.lastLoadedFragLevel=-1,this.firstSelection=-1,this.lastLevelLoadSec=0,this.fragCurrent=this.partCurrent=null,this.onLevelsUpdated(),this.clearTimer()}onLevelsUpdated(){this.lastLoadedFragLevel>-1&&this.fragCurrent&&(this.lastLoadedFragLevel=this.fragCurrent.level),this._nextAutoLevel=-1,this.onMaxAutoLevelUpdated(),this.codecTiers=null,this.audioTracksByGroup=null}onMaxAutoLevelUpdated(){this.firstSelection=-1,this.nextAutoLevelKey=""}onFragLoading(t,e){const s=e.frag;if(!this.ignoreFragment(s)){var i;if(!s.bitrateTest)this.fragCurrent=s,this.partCurrent=null!=(i=e.part)?i:null;this.clearTimer(),this.timer=self.setInterval(this._abandonRulesCheck,100)}}onLevelSwitching(t,e){this.clearTimer()}onError(t,e){if(!e.fatal)switch(e.details){case y.BUFFER_ADD_CODEC_ERROR:case y.BUFFER_APPEND_ERROR:this.lastLoadedFragLevel=-1,this.firstSelection=-1;break;case y.FRAG_LOAD_TIMEOUT:{const t=e.frag,{fragCurrent:s,partCurrent:i}=this;if(t&&s&&t.sn===s.sn&&t.level===s.level){const e=performance.now(),s=i?i.stats:t.stats,r=e-s.loading.start,n=s.loading.first?s.loading.first-s.loading.start:-1;if(s.loaded&&n>-1){const t=this.bwEstimator.getEstimateTTFB();this.bwEstimator.sample(r-Math.min(t,n),s.loaded)}else this.bwEstimator.sampleTTFB(r)}break}}}getTimeToLoadFrag(t,e,s,i){return t+s/e+(i?this.lastLevelLoadSec:0)}onLevelLoaded(t,e){const s=this.hls.config,{loading:i}=e.stats,r=i.end-i.start;f(r)&&(this.lastLevelLoadSec=r/1e3),e.details.live?this.bwEstimator.update(s.abrEwmaSlowLive,s.abrEwmaFastLive):this.bwEstimator.update(s.abrEwmaSlowVoD,s.abrEwmaFastVoD)}onFragLoaded(t,{frag:e,part:s}){const i=s?s.stats:e.stats;if(e.type===De&&this.bwEstimator.sampleTTFB(i.loading.first-i.loading.start),!this.ignoreFragment(e)){if(this.clearTimer(),e.level===this._nextAutoLevel&&(this._nextAutoLevel=-1),this.firstSelection=-1,this.hls.config.abrMaxWithRealBitrate){const t=s?s.duration:e.duration,r=this.hls.levels[e.level],n=(r.loaded?r.loaded.bytes:0)+i.loaded,a=(r.loaded?r.loaded.duration:0)+t;r.loaded={bytes:n,duration:a},r.realBitrate=Math.round(8*n/a)}if(e.bitrateTest){const t={stats:i,frag:e,part:s,id:e.type};this.onFragBuffered(p.FRAG_BUFFERED,t),e.bitrateTest=!1}else this.lastLoadedFragLevel=e.level}}onFragBuffered(t,e){const{frag:s,part:i}=e,r=null!=i&&i.stats.loaded?i.stats:s.stats;if(r.aborted)return;if(this.ignoreFragment(s))return;const n=r.parsing.end-r.loading.start-Math.min(r.loading.first-r.loading.start,this.bwEstimator.getEstimateTTFB());this.bwEstimator.sample(n,r.loaded),r.bwEstimate=this.getBwEstimate(),s.bitrateTest?this.bitrateTestDelay=n/1e3:this.bitrateTestDelay=0}ignoreFragment(t){return t.type!==De||"initSegment"===t.sn}clearTimer(){this.timer>-1&&(self.clearInterval(this.timer),this.timer=-1)}get firstAutoLevel(){const{maxAutoLevel:t,minAutoLevel:e}=this.hls,s=this.getBwEstimate(),i=this.hls.config.maxStarvationDelay,r=this.findBestLevel(s,e,t,0,i,1,1);if(r>-1)return r;const n=this.hls.firstLevel,a=Math.min(Math.max(n,e),t);return A.warn(`[abr] Could not find best starting auto level. Defaulting to first in playlist ${n} clamped to ${a}`),a}get forcedAutoLevel(){return this.nextAutoLevelKey?-1:this._nextAutoLevel}get nextAutoLevel(){const t=this.forcedAutoLevel,e=this.bwEstimator.canEstimate(),s=this.lastLoadedFragLevel>-1;if(!(-1===t||e&&s&&this.nextAutoLevelKey!==this.getAutoLevelKey()))return t;const i=e&&s?this.getNextABRAutoLevel():this.firstAutoLevel;if(-1!==t){const e=this.hls.levels;if(e.length>Math.max(t,i)&&e[t].loadError<=e[i].loadError)return t}return this._nextAutoLevel=i,this.nextAutoLevelKey=this.getAutoLevelKey(),i}getAutoLevelKey(){return`${this.getBwEstimate()}_${this.getStarvationDelay().toFixed(2)}`}getNextABRAutoLevel(){const{fragCurrent:t,partCurrent:e,hls:s}=this,{maxAutoLevel:i,config:r,minAutoLevel:n}=s,a=e?e.duration:t?t.duration:0,o=this.getBwEstimate(),l=this.getStarvationDelay();let h=r.abrBandWidthFactor,d=r.abrBandWidthUpFactor;if(l){const t=this.findBestLevel(o,n,i,l,0,h,d);if(t>=0)return t}let c=a?Math.min(a,r.maxStarvationDelay):r.maxStarvationDelay;if(!l){const t=this.bitrateTestDelay;if(t){c=(a?Math.min(a,r.maxLoadingDelay):r.maxLoadingDelay)-t,A.info(`[abr] bitrate test took ${Math.round(1e3*t)}ms, set first fragment max fetchDuration to ${Math.round(1e3*c)} ms`),h=d=1}}const u=this.findBestLevel(o,n,i,l,c,h,d);if(A.info(`[abr] ${l?"rebuffering expected":"buffer is empty"}, optimal quality level ${u}`),u>-1)return u;const f=s.levels[n],g=s.levels[s.loadLevel];return(null==f?void 0:f.bitrate)<(null==g?void 0:g.bitrate)?n:s.loadLevel}getStarvationDelay(){const t=this.hls,e=t.media;if(!e)return 1/0;const s=e&&0!==e.playbackRate?Math.abs(e.playbackRate):1,i=t.mainForwardBufferInfo;return(i?i.len:0)/s}getBwEstimate(){return this.bwEstimator.canEstimate()?this.bwEstimator.getEstimate():this.hls.config.abrEwmaDefaultEstimate}findBestLevel(t,e,s,i,r,n,a){var o;const l=i+r,h=this.lastLoadedFragLevel,d=-1===h?this.hls.firstLevel:h,{fragCurrent:c,partCurrent:u}=this,{levels:g,allAudioTracks:m,loadLevel:p,config:v}=this.hls;if(1===g.length)return 0;const y=g[d],E=!(null==y||null==(o=y.details)||!o.live),T=-1===p||-1===h;let S,L="SDR",R=(null==y?void 0:y.frameRate)||0;const{audioPreference:b,videoPreference:k}=v,w=this.audioTracksByGroup||(this.audioTracksByGroup=function(t){return t.reduce(((t,e)=>{let s=t.groups[e.groupId];s||(s=t.groups[e.groupId]={tracks:[],channels:{2:0},hasDefault:!1,hasAutoSelect:!1}),s.tracks.push(e);const i=e.channels||"2";return s.channels[i]=(s.channels[i]||0)+1,s.hasDefault=s.hasDefault||e.default,s.hasAutoSelect=s.hasAutoSelect||e.autoselect,s.hasDefault&&(t.hasDefaultAudio=!0),s.hasAutoSelect&&(t.hasAutoSelectAudio=!0),t}),{hasDefaultAudio:!1,hasAutoSelectAudio:!1,groups:{}})}(m));if(T){if(-1!==this.firstSelection)return this.firstSelection;const i=this.codecTiers||(this.codecTiers=function(t,e,s,i){return t.slice(s,i+1).reduce(((t,s)=>{if(!s.codecSet)return t;const i=s.audioGroups;let r=t[s.codecSet];r||(t[s.codecSet]=r={minBitrate:1/0,minHeight:1/0,minFramerate:1/0,maxScore:0,videoRanges:{SDR:0},channels:{2:0},hasDefaultAudio:!i,fragmentError:0}),r.minBitrate=Math.min(r.minBitrate,s.bitrate);const n=Math.min(s.height,s.width);return r.minHeight=Math.min(r.minHeight,n),r.minFramerate=Math.min(r.minFramerate,s.frameRate),r.maxScore=Math.max(r.maxScore,s.score),r.fragmentError+=s.fragmentError,r.videoRanges[s.videoRange]=(r.videoRanges[s.videoRange]||0)+1,i&&i.forEach((t=>{if(!t)return;const s=e.groups[t];s&&(r.hasDefaultAudio=r.hasDefaultAudio||e.hasDefaultAudio?s.hasDefault:s.hasAutoSelect||!e.hasDefaultAudio&&!e.hasAutoSelectAudio,Object.keys(s.channels).forEach((t=>{r.channels[t]=(r.channels[t]||0)+s.channels[t]})))})),t}),{})}(g,w,e,s)),r=function(t,e,s,i,r){const n=Object.keys(t),a=null==i?void 0:i.channels,o=null==i?void 0:i.audioCodec,l=a&&2===parseInt(a);let h=!0,d=!1,c=1/0,u=1/0,g=1/0,m=0,p=[];const{preferHDR:v,allowedVideoRanges:y}=Ms(e,r);for(let e=n.length;e--;){const s=t[n[e]];h=s.channels[2]>0,c=Math.min(c,s.minHeight),u=Math.min(u,s.minFramerate),g=Math.min(g,s.minBitrate);const i=y.filter((t=>s.videoRanges[t]>0));i.length>0&&(d=!0,p=i)}c=f(c)?c:0,u=f(u)?u:0;const E=Math.max(1080,c),T=Math.max(30,u);return g=f(g)?g:s,s=Math.max(g,s),d||(e=void 0,p=[]),{codecSet:n.reduce(((e,i)=>{const r=t[i];if(i===e)return e;if(r.minBitrate>s)return Fs(i,`min bitrate of ${r.minBitrate} > current estimate of ${s}`),e;if(!r.hasDefaultAudio)return Fs(i,"no renditions with default or auto-select sound found"),e;if(o&&i.indexOf(o.substring(0,4))%5!=0)return Fs(i,`audio codec preference "${o}" not found`),e;if(a&&!l){if(!r.channels[a])return Fs(i,`no renditions with ${a} channel sound found (channels options: ${Object.keys(r.channels)})`),e}else if((!o||l)&&h&&0===r.channels[2])return Fs(i,"no renditions with stereo sound found"),e;return r.minHeight>E?(Fs(i,`min resolution of ${r.minHeight} > maximum of ${E}`),e):r.minFramerate>T?(Fs(i,`min framerate of ${r.minFramerate} > maximum of ${T}`),e):p.some((t=>r.videoRanges[t]>0))?r.maxScore=ae(e)||r.fragmentError>t[e].fragmentError)?e:(m=r.maxScore,i):(Fs(i,`no variants with VIDEO-RANGE of ${JSON.stringify(p)} found`),e)}),void 0),videoRanges:p,preferHDR:v,minFramerate:u,minBitrate:g}}(i,L,t,b,k),{codecSet:n,videoRanges:a,minFramerate:o,minBitrate:l,preferHDR:h}=r;S=n,L=h?a[a.length-1]:a[0],R=o,t=Math.max(t,l),A.log(`[abr] picked start tier ${JSON.stringify(r)}`)}else S=null==y?void 0:y.codecSet,L=null==y?void 0:y.videoRange;const D=u?u.duration:c?c.duration:0,I=this.bwEstimator.getEstimateTTFB()/1e3,_=[];for(let o=s;o>=e;o--){var C;const e=g[o],c=o>d;if(!e)continue;if(v.useMediaCapabilities&&!e.supportedResult&&!e.supportedPromise){const s=navigator.mediaCapabilities;"function"==typeof(null==s?void 0:s.decodingInfo)&&xs(e,w,L,R,t,b)?(e.supportedPromise=Ps(e,w,s),e.supportedPromise.then((t=>{if(!this.hls)return;e.supportedResult=t;const s=this.hls.levels,i=s.indexOf(e);t.error?A.warn(`[abr] MediaCapabilities decodingInfo error: "${t.error}" for level ${i} ${JSON.stringify(t)}`):t.supported||(A.warn(`[abr] Unsupported MediaCapabilities decodingInfo result for level ${i} ${JSON.stringify(t)}`),i>-1&&s.length>1&&(A.log(`[abr] Removing unsupported level ${i}`),this.hls.removeLevel(i)))}))):e.supportedResult=_s}if(S&&e.codecSet!==S||L&&e.videoRange!==L||c&&R>e.frameRate||!c&&R>0&&R=2*D&&0===r?g[o].averageBitrate:g[o].maxBitrate,M=this.getTimeToLoadFrag(I,x,P*k,void 0===m);if(x>=P&&(o===h||0===e.loadError&&0===e.fragmentError)&&(M<=I||!f(M)||E&&!this.bitrateTestDelay||M${o} adjustedbw(${Math.round(x)})-bitrate=${Math.round(x-P)} ttfb:${I.toFixed(1)} avgDuration:${k.toFixed(1)} maxFetchDuration:${l.toFixed(1)} fetchDuration:${M.toFixed(1)} firstSelection:${T} codecSet:${S} videoRange:${L} hls.loadLevel:${p}`)),T&&(this.firstSelection=o),o}}return-1}set nextAutoLevel(t){const{maxAutoLevel:e,minAutoLevel:s}=this.hls,i=Math.min(Math.max(t,s),e);this._nextAutoLevel!==i&&(this.nextAutoLevelKey="",this._nextAutoLevel=i)}},bufferController:class{constructor(t){this.details=null,this._objectUrl=null,this.operationQueue=void 0,this.listeners=void 0,this.hls=void 0,this.bufferCodecEventsExpected=0,this._bufferCodecEventsTotal=0,this.media=null,this.mediaSource=null,this.lastMpegAudioChunk=null,this.appendSource=void 0,this.appendErrors={audio:0,video:0,audiovideo:0},this.tracks={},this.pendingTracks={},this.sourceBuffer=void 0,this.log=void 0,this.warn=void 0,this.error=void 0,this._onEndStreaming=t=>{this.hls&&this.hls.pauseBuffering()},this._onStartStreaming=t=>{this.hls&&this.hls.resumeBuffering()},this._onMediaSourceOpen=()=>{const{media:t,mediaSource:e}=this;this.log("Media source opened"),t&&(t.removeEventListener("emptied",this._onMediaEmptied),this.updateMediaElementDuration(),this.hls.trigger(p.MEDIA_ATTACHED,{media:t,mediaSource:e})),e&&e.removeEventListener("sourceopen",this._onMediaSourceOpen),this.checkPendingTracks()},this._onMediaSourceClose=()=>{this.log("Media source closed")},this._onMediaSourceEnded=()=>{this.log("Media source ended")},this._onMediaEmptied=()=>{const{mediaSrc:t,_objectUrl:e}=this;t!==e&&A.error(`Media element src was set while attaching MediaSource (${e} > ${t})`)},this.hls=t;const e="[buffer-controller]";var s;this.appendSource=(s=te(t.config.preferManagedMediaSource),"undefined"!=typeof self&&s===self.ManagedMediaSource),this.log=A.log.bind(A,e),this.warn=A.warn.bind(A,e),this.error=A.error.bind(A,e),this._initSourceBuffer(),this.registerListeners()}hasSourceTypes(){return this.getSourceBufferTypes().length>0||Object.keys(this.pendingTracks).length>0}destroy(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=null,this.hls=null}registerListeners(){const{hls:t}=this;t.on(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.BUFFER_RESET,this.onBufferReset,this),t.on(p.BUFFER_APPENDING,this.onBufferAppending,this),t.on(p.BUFFER_CODECS,this.onBufferCodecs,this),t.on(p.BUFFER_EOS,this.onBufferEos,this),t.on(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(p.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(p.FRAG_PARSED,this.onFragParsed,this),t.on(p.FRAG_CHANGED,this.onFragChanged,this)}unregisterListeners(){const{hls:t}=this;t.off(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.BUFFER_RESET,this.onBufferReset,this),t.off(p.BUFFER_APPENDING,this.onBufferAppending,this),t.off(p.BUFFER_CODECS,this.onBufferCodecs,this),t.off(p.BUFFER_EOS,this.onBufferEos,this),t.off(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(p.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(p.FRAG_PARSED,this.onFragParsed,this),t.off(p.FRAG_CHANGED,this.onFragChanged,this)}_initSourceBuffer(){this.sourceBuffer={},this.operationQueue=new Or(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]},this.appendErrors={audio:0,video:0,audiovideo:0},this.lastMpegAudioChunk=null}onManifestLoading(){this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=0,this.details=null}onManifestParsed(t,e){let s=2;(e.audio&&!e.video||!e.altAudio)&&(s=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=s,this.log(`${this.bufferCodecEventsExpected} bufferCodec event(s) expected`)}onMediaAttaching(t,e){const s=this.media=e.media,i=te(this.appendSource);if(s&&i){var r;const t=this.mediaSource=new i;this.log(`created media source: ${null==(r=t.constructor)?void 0:r.name}`),t.addEventListener("sourceopen",this._onMediaSourceOpen),t.addEventListener("sourceended",this._onMediaSourceEnded),t.addEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(t.addEventListener("startstreaming",this._onStartStreaming),t.addEventListener("endstreaming",this._onEndStreaming));const e=this._objectUrl=self.URL.createObjectURL(t);if(this.appendSource)try{s.removeAttribute("src");const i=self.ManagedMediaSource;s.disableRemotePlayback=s.disableRemotePlayback||i&&t instanceof i,Ur(s),function(t,e){const s=self.document.createElement("source");s.type="video/mp4",s.src=e,t.appendChild(s)}(s,e),s.load()}catch(t){s.src=e}else s.src=e;s.addEventListener("emptied",this._onMediaEmptied)}}onMediaDetaching(){const{media:t,mediaSource:e,_objectUrl:s}=this;if(e){if(this.log("media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){this.warn(`onMediaDetaching: ${t.message} while calling endOfStream`)}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),this.appendSource&&(e.removeEventListener("startstreaming",this._onStartStreaming),e.removeEventListener("endstreaming",this._onEndStreaming)),t&&(t.removeEventListener("emptied",this._onMediaEmptied),s&&self.URL.revokeObjectURL(s),this.mediaSrc===s?(t.removeAttribute("src"),this.appendSource&&Ur(t),t.load()):this.warn("media|source.src was changed by a third party - skip cleanup")),this.mediaSource=null,this.media=null,this._objectUrl=null,this.bufferCodecEventsExpected=this._bufferCodecEventsTotal,this.pendingTracks={},this.tracks={}}this.hls.trigger(p.MEDIA_DETACHED,void 0)}onBufferReset(){this.getSourceBufferTypes().forEach((t=>{this.resetBuffer(t)})),this._initSourceBuffer()}resetBuffer(t){const e=this.sourceBuffer[t];try{var s;if(e)this.removeBufferListeners(t),this.sourceBuffer[t]=void 0,null!=(s=this.mediaSource)&&s.sourceBuffers.length&&this.mediaSource.removeSourceBuffer(e)}catch(e){this.warn(`onBufferReset ${t}`,e)}}onBufferCodecs(t,e){const s=this.getSourceBufferTypes().length,i=Object.keys(e);if(i.forEach((t=>{if(s){const s=this.tracks[t];if(s&&"function"==typeof s.buffer.changeType){var i;const{id:r,codec:n,levelCodec:a,container:o,metadata:l}=e[t],h=de(s.codec,s.levelCodec),d=null==h?void 0:h.replace(Nr,"$1");let c=de(n,a);const u=null==(i=c)?void 0:i.replace(Nr,"$1");if(c&&d!==u){"audio"===t.slice(0,5)&&(c=he(c,this.appendSource));const e=`${o};codecs=${c}`;this.appendChangeType(t,e),this.log(`switching codec ${h} to ${c}`),this.tracks[t]={buffer:s.buffer,codec:n,container:o,levelCodec:a,metadata:l,id:r}}}}else this.pendingTracks[t]=e[t]})),s)return;const r=Math.max(this.bufferCodecEventsExpected-1,0);this.bufferCodecEventsExpected!==r&&(this.log(`${r} bufferCodec event(s) expected ${i.join(",")}`),this.bufferCodecEventsExpected=r),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks()}appendChangeType(t,e){const{operationQueue:s}=this,i={execute:()=>{const i=this.sourceBuffer[t];i&&(this.log(`changing ${t} sourceBuffer type to ${e}`),i.changeType(e)),s.shiftAndExecuteNext(t)},onStart:()=>{},onComplete:()=>{},onError:e=>{this.warn(`Failed to change ${t} SourceBuffer type`,e)}};s.append(i,t,!!this.pendingTracks[t])}onBufferAppending(t,e){const{hls:s,operationQueue:i,tracks:r}=this,{data:n,type:a,frag:o,part:l,chunkMeta:h}=e,d=h.buffering[a],c=self.performance.now();d.start=c;const u=o.stats.buffering,f=l?l.stats.buffering:null;0===u.start&&(u.start=c),f&&0===f.start&&(f.start=c);const g=r.audio;let m=!1;"audio"===a&&"audio/mpeg"===(null==g?void 0:g.container)&&(m=!this.lastMpegAudioChunk||1===h.id||this.lastMpegAudioChunk.sn!==h.sn,this.lastMpegAudioChunk=h);const E=o.start,T={execute:()=>{if(d.executeStart=self.performance.now(),m){const t=this.sourceBuffer[a];if(t){const e=E-t.timestampOffset;Math.abs(e)>=.1&&(this.log(`Updating audio SourceBuffer timestampOffset to ${E} (delta: ${e}) sn: ${o.sn})`),t.timestampOffset=E)}}this.appendExecutor(n,a)},onStart:()=>{},onComplete:()=>{const t=self.performance.now();d.executeEnd=d.end=t,0===u.first&&(u.first=t),f&&0===f.first&&(f.first=t);const{sourceBuffer:e}=this,s={};for(const t in e)s[t]=Xs.getBuffered(e[t]);this.appendErrors[a]=0,"audio"===a||"video"===a?this.appendErrors.audiovideo=0:(this.appendErrors.audio=0,this.appendErrors.video=0),this.hls.trigger(p.BUFFER_APPENDED,{type:a,frag:o,part:l,chunkMeta:h,parent:o.type,timeRanges:s})},onError:t=>{const e={type:v.MEDIA_ERROR,parent:o.type,details:y.BUFFER_APPEND_ERROR,sourceBufferName:a,frag:o,part:l,chunkMeta:h,error:t,err:t,fatal:!1};if(t.code===DOMException.QUOTA_EXCEEDED_ERR)e.details=y.BUFFER_FULL_ERROR;else{const t=++this.appendErrors[a];e.details=y.BUFFER_APPEND_ERROR,this.warn(`Failed ${t}/${s.config.appendErrorMaxRetry} times to append segment in "${a}" sourceBuffer`),t>=s.config.appendErrorMaxRetry&&(e.fatal=!0)}s.trigger(p.ERROR,e)}};i.append(T,a,!!this.pendingTracks[a])}onBufferFlushing(t,e){const{operationQueue:s}=this,i=t=>({execute:this.removeExecutor.bind(this,t,e.startOffset,e.endOffset),onStart:()=>{},onComplete:()=>{this.hls.trigger(p.BUFFER_FLUSHED,{type:t})},onError:e=>{this.warn(`Failed to remove from ${t} SourceBuffer`,e)}});e.type?s.append(i(e.type),e.type):this.getSourceBufferTypes().forEach((t=>{s.append(i(t),t)}))}onFragParsed(t,e){const{frag:s,part:i}=e,r=[],n=i?i.elementaryStreams:s.elementaryStreams;n[x]?r.push("audiovideo"):(n[_]&&r.push("audio"),n[C]&&r.push("video"));0===r.length&&this.warn(`Fragments must have at least one ElementaryStreamType set. type: ${s.type} level: ${s.level} sn: ${s.sn}`),this.blockBuffers((()=>{const t=self.performance.now();s.stats.buffering.end=t,i&&(i.stats.buffering.end=t);const e=i?i.stats:s.stats;this.hls.trigger(p.FRAG_BUFFERED,{frag:s,part:i,stats:e,id:s.type})}),r)}onFragChanged(t,e){this.trimBuffers()}onBufferEos(t,e){this.getSourceBufferTypes().reduce(((t,s)=>{const i=this.sourceBuffer[s];return!i||e.type&&e.type!==s||(i.ending=!0,i.ended||(i.ended=!0,this.log(`${s} sourceBuffer now EOS`))),t&&!(i&&!i.ended)}),!0)&&(this.log("Queueing mediaSource.endOfStream()"),this.blockBuffers((()=>{this.getSourceBufferTypes().forEach((t=>{const e=this.sourceBuffer[t];e&&(e.ending=!1)}));const{mediaSource:t}=this;t&&"open"===t.readyState?(this.log("Calling mediaSource.endOfStream()"),t.endOfStream()):t&&this.log(`Could not call mediaSource.endOfStream(). mediaSource.readyState: ${t.readyState}`)})))}onLevelUpdated(t,{details:e}){e.fragments.length&&(this.details=e,this.getSourceBufferTypes().length?this.blockBuffers(this.updateMediaElementDuration.bind(this)):this.updateMediaElementDuration())}trimBuffers(){const{hls:t,details:e,media:s}=this;if(!s||null===e)return;if(!this.getSourceBufferTypes().length)return;const i=t.config,r=s.currentTime,n=e.levelTargetDuration,a=e.live&&null!==i.liveBackBufferLength?i.liveBackBufferLength:i.backBufferLength;if(f(a)&&a>0){const t=Math.max(a,n),e=Math.floor(r/n)*n-t;this.flushBackBuffer(r,n,e)}if(f(i.frontBufferFlushThreshold)&&i.frontBufferFlushThreshold>0){const t=Math.max(i.maxBufferLength,i.frontBufferFlushThreshold),e=Math.max(t,n),s=Math.floor(r/n)*n+e;this.flushFrontBuffer(r,n,s)}}flushBackBuffer(t,e,s){const{details:i,sourceBuffer:r}=this;this.getSourceBufferTypes().forEach((n=>{const a=r[n];if(a){const r=Xs.getBuffered(a);if(r.length>0&&s>r.start(0)){if(this.hls.trigger(p.BACK_BUFFER_REACHED,{bufferEnd:s}),null!=i&&i.live)this.hls.trigger(p.LIVE_BACK_BUFFER_REACHED,{bufferEnd:s});else if(a.ended&&r.end(r.length-1)-t<2*e)return void this.log(`Cannot flush ${n} back buffer while SourceBuffer is in ended state`);this.hls.trigger(p.BUFFER_FLUSHING,{startOffset:0,endOffset:s,type:n})}}}))}flushFrontBuffer(t,e,s){const{sourceBuffer:i}=this;this.getSourceBufferTypes().forEach((r=>{const n=i[r];if(n){const i=Xs.getBuffered(n),a=i.length;if(a<2)return;const o=i.start(a-1),l=i.end(a-1);if(s>o||t>=o&&t<=l)return;if(n.ended&&t-l<2*e)return void this.log(`Cannot flush ${r} front buffer while SourceBuffer is in ended state`);this.hls.trigger(p.BUFFER_FLUSHING,{startOffset:o,endOffset:1/0,type:r})}}))}updateMediaElementDuration(){if(!this.details||!this.media||!this.mediaSource||"open"!==this.mediaSource.readyState)return;const{details:t,hls:e,media:s,mediaSource:i}=this,r=t.fragments[0].start+t.totalduration,n=s.duration,a=f(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(i.duration=1/0,this.updateSeekableRange(t)):(r>a&&r>n||!f(n))&&(this.log(`Updating Media Source duration to ${r.toFixed(3)}`),i.duration=r)}updateSeekableRange(t){const e=this.mediaSource,s=t.fragments;if(s.length&&t.live&&null!=e&&e.setLiveSeekableRange){const i=Math.max(0,s[0].start),r=Math.max(i,i+t.totalduration);this.log(`Media Source duration is set to ${e.duration}. Setting seekable range to ${i}-${r}.`),e.setLiveSeekableRange(i,r)}}checkPendingTracks(){const{bufferCodecEventsExpected:t,operationQueue:e,pendingTracks:s}=this,i=Object.keys(s).length;if(i&&(!t||2===i||"audiovideo"in s)){this.createSourceBuffers(s),this.pendingTracks={};const t=this.getSourceBufferTypes();if(t.length)this.hls.trigger(p.BUFFER_CREATED,{tracks:this.tracks}),t.forEach((t=>{e.executeNext(t)}));else{const t=new Error("could not create source buffer for media codec(s)");this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:t,reason:t.message})}}}createSourceBuffers(t){const{sourceBuffer:e,mediaSource:s}=this;if(!s)throw Error("createSourceBuffers called when mediaSource was null");for(const r in t)if(!e[r]){var i;const n=t[r];if(!n)throw Error(`source buffer exists for track ${r}, however track does not`);let a=-1===(null==(i=n.levelCodec)?void 0:i.indexOf(","))?n.levelCodec:n.codec;a&&"audio"===r.slice(0,5)&&(a=he(a,this.appendSource));const o=`${n.container};codecs=${a}`;this.log(`creating sourceBuffer(${o})`);try{const t=e[r]=s.addSourceBuffer(o),i=r;this.addBufferListener(i,"updatestart",this._onSBUpdateStart),this.addBufferListener(i,"updateend",this._onSBUpdateEnd),this.addBufferListener(i,"error",this._onSBUpdateError),this.appendSource&&this.addBufferListener(i,"bufferedchange",((t,e)=>{const s=e.removedRanges;null!=s&&s.length&&this.hls.trigger(p.BUFFER_FLUSHED,{type:r})})),this.tracks[r]={buffer:t,codec:a,container:n.container,levelCodec:n.levelCodec,metadata:n.metadata,id:n.id}}catch(t){this.error(`error while trying to add sourceBuffer: ${t.message}`),this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,sourceBufferName:r,mimeType:o})}}}get mediaSrc(){var t,e;const s=(null==(t=this.media)||null==(e=t.querySelector)?void 0:e.call(t,"source"))||this.media;return null==s?void 0:s.src}_onSBUpdateStart(t){const{operationQueue:e}=this;e.current(t).onStart()}_onSBUpdateEnd(t){var e;if("closed"===(null==(e=this.mediaSource)?void 0:e.readyState))return void this.resetBuffer(t);const{operationQueue:s}=this;s.current(t).onComplete(),s.shiftAndExecuteNext(t)}_onSBUpdateError(t,e){var s;const i=new Error(`${t} SourceBuffer error. MediaSource readyState: ${null==(s=this.mediaSource)?void 0:s.readyState}`);this.error(`${i}`,e),this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.BUFFER_APPENDING_ERROR,sourceBufferName:t,error:i,fatal:!1});const r=this.operationQueue.current(t);r&&r.onError(i)}removeExecutor(t,e,s){const{media:i,mediaSource:r,operationQueue:n,sourceBuffer:a}=this,o=a[t];if(!i||!r||!o)return this.warn(`Attempting to remove from the ${t} SourceBuffer, but it does not exist`),void n.shiftAndExecuteNext(t);const l=f(i.duration)?i.duration:1/0,h=f(r.duration)?r.duration:1/0,d=Math.max(0,e),c=Math.min(s,l,h);c>d&&(!o.ending||o.ended)?(o.ended=!1,this.log(`Removing [${d},${c}] from the ${t} SourceBuffer`),o.remove(d,c)):n.shiftAndExecuteNext(t)}appendExecutor(t,e){const s=this.sourceBuffer[e];if(s)s.ended=!1,s.appendBuffer(t);else if(!this.pendingTracks[e])throw new Error(`Attempting to append to the ${e} SourceBuffer, but it does not exist`)}blockBuffers(t,e=this.getSourceBufferTypes()){if(!e.length)return this.log("Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve().then(t);const{operationQueue:s}=this,i=e.map((t=>s.appendBlocker(t)));Promise.all(i).then((()=>{t(),e.forEach((t=>{const e=this.sourceBuffer[t];null!=e&&e.updating||s.shiftAndExecuteNext(t)}))}))}getSourceBufferTypes(){return Object.keys(this.sourceBuffer)}addBufferListener(t,e,s){const i=this.sourceBuffer[t];if(!i)return;const r=s.bind(this,t);this.listeners[t].push({event:e,listener:r}),i.addEventListener(e,r)}removeBufferListeners(t){const e=this.sourceBuffer[t];e&&this.listeners[t].forEach((t=>{e.removeEventListener(t.event,t.listener)}))}},capLevelController:Mn,errorController:class{constructor(t){this.hls=void 0,this.playlistError=0,this.penalizedRenditions={},this.log=void 0,this.warn=void 0,this.error=void 0,this.hls=t,this.log=A.log.bind(A,"[info]:"),this.warn=A.warn.bind(A,"[warning]:"),this.error=A.error.bind(A,"[error]:"),this.registerListeners()}registerListeners(){const t=this.hls;t.on(p.ERROR,this.onError,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.LEVEL_UPDATED,this.onLevelUpdated,this)}unregisterListeners(){const t=this.hls;t&&(t.off(p.ERROR,this.onError,this),t.off(p.ERROR,this.onErrorOut,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.LEVEL_UPDATED,this.onLevelUpdated,this))}destroy(){this.unregisterListeners(),this.hls=null,this.penalizedRenditions={}}startLoad(t){}stopLoad(){this.playlistError=0}getVariantLevelIndex(t){return(null==t?void 0:t.type)===De?t.level:this.hls.loadLevel}onManifestLoading(){this.playlistError=0,this.penalizedRenditions={}}onLevelUpdated(){this.playlistError=0}onError(t,e){var s,i;if(e.fatal)return;const r=this.hls,n=e.context;switch(e.details){case y.FRAG_LOAD_ERROR:case y.FRAG_LOAD_TIMEOUT:case y.KEY_LOAD_ERROR:case y.KEY_LOAD_TIMEOUT:return void(e.errorAction=this.getFragRetryOrSwitchAction(e));case y.FRAG_PARSING_ERROR:if(null!=(s=e.frag)&&s.gap)return void(e.errorAction={action:Ts,flags:Rs});case y.FRAG_GAP:case y.FRAG_DECRYPT_ERROR:return e.errorAction=this.getFragRetryOrSwitchAction(e),void(e.errorAction.action=Ss);case y.LEVEL_EMPTY_ERROR:case y.LEVEL_PARSING_ERROR:{var a,o;const t=e.parent===De?e.level:r.loadLevel;e.details===y.LEVEL_EMPTY_ERROR&&null!=(a=e.context)&&null!=(o=a.levelDetails)&&o.live?e.errorAction=this.getPlaylistRetryOrSwitchAction(e,t):(e.levelRetry=!1,e.errorAction=this.getLevelSwitchAction(e,t))}return;case y.LEVEL_LOAD_ERROR:case y.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==n?void 0:n.level)&&(e.errorAction=this.getPlaylistRetryOrSwitchAction(e,n.level)));case y.AUDIO_TRACK_LOAD_ERROR:case y.AUDIO_TRACK_LOAD_TIMEOUT:case y.SUBTITLE_LOAD_ERROR:case y.SUBTITLE_TRACK_LOAD_TIMEOUT:if(n){const t=r.levels[r.loadLevel];if(t&&(n.type===ke&&t.hasAudioGroup(n.groupId)||n.type===we&&t.hasSubtitleGroup(n.groupId)))return e.errorAction=this.getPlaylistRetryOrSwitchAction(e,r.loadLevel),e.errorAction.action=Ss,void(e.errorAction.flags=bs)}return;case y.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:{const t=r.levels[r.loadLevel],s=null==t?void 0:t.attrs["HDCP-LEVEL"];s?e.errorAction={action:Ss,flags:ks,hdcpLevel:s}:this.keySystemError(e)}return;case y.BUFFER_ADD_CODEC_ERROR:case y.REMUX_ALLOC_ERROR:case y.BUFFER_APPEND_ERROR:return void(e.errorAction=this.getLevelSwitchAction(e,null!=(i=e.level)?i:r.loadLevel));case y.INTERNAL_EXCEPTION:case y.BUFFER_APPENDING_ERROR:case y.BUFFER_FULL_ERROR:case y.LEVEL_SWITCH_ERROR:case y.BUFFER_STALLED_ERROR:case y.BUFFER_SEEK_OVER_HOLE:case y.BUFFER_NUDGE_ON_STALL:return void(e.errorAction={action:Ts,flags:Rs})}e.type===v.KEY_SYSTEM_ERROR&&this.keySystemError(e)}keySystemError(t){const e=this.getVariantLevelIndex(t.frag);t.levelRetry=!1,t.errorAction=this.getLevelSwitchAction(t,e)}getPlaylistRetryOrSwitchAction(t,e){const s=us(this.hls.config.playlistLoadPolicy,t),i=this.playlistError++;if(ms(s,i,cs(t),t.response))return{action:As,flags:Rs,retryConfig:s,retryCount:i};const r=this.getLevelSwitchAction(t,e);return s&&(r.retryConfig=s,r.retryCount=i),r}getFragRetryOrSwitchAction(t){const e=this.hls,s=this.getVariantLevelIndex(t.frag),i=e.levels[s],{fragLoadPolicy:r,keyLoadPolicy:n}=e.config,a=us(t.details.startsWith("key")?n:r,t),o=e.levels.reduce(((t,e)=>t+e.fragmentError),0);if(i){t.details!==y.FRAG_GAP&&i.fragmentError++;if(ms(a,o,cs(t),t.response))return{action:As,flags:Rs,retryConfig:a,retryCount:o}}const l=this.getLevelSwitchAction(t,s);return a&&(l.retryConfig=a,l.retryCount=o),l}getLevelSwitchAction(t,e){const s=this.hls;null==e&&(e=s.loadLevel);const i=this.hls.levels[e];if(i){var r,n;const e=t.details;i.loadError++,e===y.BUFFER_APPEND_ERROR&&i.fragmentError++;let l=-1;const{levels:h,loadLevel:d,minAutoLevel:c,maxAutoLevel:u}=s;s.autoLevelEnabled||(s.loadLevel=-1);const f=null==(r=t.frag)?void 0:r.type,g=(f===Ie&&e===y.FRAG_PARSING_ERROR||"audio"===t.sourceBufferName&&(e===y.BUFFER_ADD_CODEC_ERROR||e===y.BUFFER_APPEND_ERROR))&&h.some((({audioCodec:t})=>i.audioCodec!==t)),m="video"===t.sourceBufferName&&(e===y.BUFFER_ADD_CODEC_ERROR||e===y.BUFFER_APPEND_ERROR)&&h.some((({codecSet:t,audioCodec:e})=>i.codecSet!==t&&i.audioCodec===e)),{type:p,groupId:v}=null!=(n=t.context)?n:{};for(let s=h.length;s--;){const r=(s+d)%h.length;if(r!==d&&r>=c&&r<=u&&0===h[r].loadError){var a,o;const s=h[r];if(e===y.FRAG_GAP&&f===De&&t.frag){const e=h[r].details;if(e){const s=vs(t.frag,e.fragments,t.frag.start);if(null!=s&&s.gap)continue}}else{if(p===ke&&s.hasAudioGroup(v)||p===we&&s.hasSubtitleGroup(v))continue;if(f===Ie&&null!=(a=i.audioGroups)&&a.some((t=>s.hasAudioGroup(t)))||f===_e&&null!=(o=i.subtitleGroups)&&o.some((t=>s.hasSubtitleGroup(t)))||g&&i.audioCodec===s.audioCodec||!g&&i.audioCodec!==s.audioCodec||m&&i.codecSet===s.codecSet)continue}l=r;break}}if(l>-1&&s.loadLevel!==l)return t.levelRetry=!0,this.playlistError=0,{action:Ss,flags:Rs,nextAutoLevel:l}}return{action:Ss,flags:bs}}onErrorOut(t,e){var s;switch(null==(s=e.errorAction)?void 0:s.action){case Ts:break;case Ss:this.sendAlternateToPenaltyBox(e),e.errorAction.resolved||e.details===y.FRAG_GAP?/MediaSource readyState: ended/.test(e.error.message)&&(this.warn(`MediaSource ended after "${e.sourceBufferName}" sourceBuffer append error. Attempting to recover from media error.`),this.hls.recoverMediaError()):e.fatal=!0}e.fatal&&this.hls.stopLoad()}sendAlternateToPenaltyBox(t){const e=this.hls,s=t.errorAction;if(!s)return;const{flags:i,hdcpLevel:r,nextAutoLevel:n}=s;switch(i){case Rs:this.switchLevel(t,n);break;case ks:r&&(e.maxHdcpLevel=qe[qe.indexOf(r)-1],s.resolved=!0),this.warn(`Restricting playback to HDCP-LEVEL of "${e.maxHdcpLevel}" or lower`)}s.resolved||this.switchLevel(t,n)}switchLevel(t,e){void 0!==e&&t.errorAction&&(this.warn(`switching to level ${e} after ${t.details}`),this.hls.nextAutoLevel=e,t.errorAction.resolved=!0,this.hls.nextLoadLevel=this.hls.nextAutoLevel)}},fpsController:class{constructor(t){this.hls=void 0,this.isVideoPlaybackQualityAvailable=!1,this.timer=void 0,this.media=null,this.lastTime=void 0,this.lastDroppedFrames=0,this.lastDecodedFrames=0,this.streamController=void 0,this.hls=t,this.registerListeners()}setStreamController(t){this.streamController=t}registerListeners(){this.hls.on(p.MEDIA_ATTACHING,this.onMediaAttaching,this)}unregisterListeners(){this.hls.off(p.MEDIA_ATTACHING,this.onMediaAttaching,this)}destroy(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null}onMediaAttaching(t,e){const s=this.hls.config;if(s.capLevelOnFPSDrop){const t=e.media instanceof self.HTMLVideoElement?e.media:null;this.media=t,t&&"function"==typeof t.getVideoPlaybackQuality&&(this.isVideoPlaybackQualityAvailable=!0),self.clearInterval(this.timer),this.timer=self.setInterval(this.checkFPSInterval.bind(this),s.fpsDroppedMonitoringPeriod)}}checkFPS(t,e,s){const i=performance.now();if(e){if(this.lastTime){const t=i-this.lastTime,r=s-this.lastDroppedFrames,n=e-this.lastDecodedFrames,a=1e3*r/t,o=this.hls;if(o.trigger(p.FPS_DROP,{currentDropped:r,currentDecoded:n,totalDroppedFrames:s}),a>0&&r>o.config.fpsDroppedMonitoringThreshold*n){let t=o.currentLevel;A.warn("drop FPS ratio greater than max allowed value for currentLevel: "+t),t>0&&(-1===o.autoLevelCapping||o.autoLevelCapping>=t)&&(t-=1,o.trigger(p.FPS_DROP_LEVEL_CAPPING,{level:t,droppedLevel:o.currentLevel}),o.autoLevelCapping=t,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=s,this.lastDecodedFrames=e}}checkFPSInterval(){const t=this.media;if(t)if(this.isVideoPlaybackQualityAvailable){const e=t.getVideoPlaybackQuality();this.checkFPS(t,e.totalVideoFrames,e.droppedVideoFrames)}else this.checkFPS(t,t.webkitDecodedFrameCount,t.webkitDroppedFrameCount)}},stretchShortVideoTrack:!1,maxAudioFramesDrift:1,forceKeyFrameOnDiscontinuity:!0,abrEwmaFastLive:3,abrEwmaSlowLive:9,abrEwmaFastVoD:3,abrEwmaSlowVoD:9,abrEwmaDefaultEstimate:5e5,abrEwmaDefaultEstimateMax:5e6,abrBandWidthFactor:.95,abrBandWidthUpFactor:.7,abrMaxWithRealBitrate:!1,maxStarvationDelay:4,maxLoadingDelay:4,minAutoBitrate:0,emeEnabled:!1,widevineLicenseUrl:void 0,drmSystems:{},drmSystemOptions:{},requestMediaKeySystemAccessFunc:tt,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableID3MetadataCues:!0,useMediaCapabilities:!0,certLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:null,errorRetry:null}},keyLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"},errorRetry:{maxNumRetry:8,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"}}},manifestLoadPolicy:{default:{maxTimeToFirstByteMs:1/0,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},playlistLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:2,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},fragLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:12e4,timeoutRetry:{maxNumRetry:4,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:6,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},steeringManifestLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},manifestLoadingTimeOut:1e4,manifestLoadingMaxRetry:1,manifestLoadingRetryDelay:1e3,manifestLoadingMaxRetryTimeout:64e3,levelLoadingTimeOut:1e4,levelLoadingMaxRetry:4,levelLoadingRetryDelay:1e3,levelLoadingMaxRetryTimeout:64e3,fragLoadingTimeOut:2e4,fragLoadingMaxRetry:6,fragLoadingRetryDelay:1e3,fragLoadingMaxRetryTimeout:64e3},{cueHandler:Sa,enableWebVTT:!0,enableIMSC1:!0,enableCEA708Captions:!0,captionsTextTrack1Label:"English",captionsTextTrack1LanguageCode:"en",captionsTextTrack2Label:"Spanish",captionsTextTrack2LanguageCode:"es",captionsTextTrack3Label:"Unknown CC",captionsTextTrack3LanguageCode:"",captionsTextTrack4Label:"Unknown CC",captionsTextTrack4LanguageCode:"",renderTextTracksNatively:!0}),{},{subtitleStreamController:class extends Ri{constructor(t,e,s){super(t,e,s,"[subtitle-stream-controller]",_e),this.currentTrackId=-1,this.tracksBuffered=[],this.mainDetails=null,this._registerListeners()}onHandlerDestroying(){this._unregisterListeners(),super.onHandlerDestroying(),this.mainDetails=null}_registerListeners(){const{hls:t}=this;t.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.LEVEL_LOADED,this.onLevelLoaded,this),t.on(p.ERROR,this.onError,this),t.on(p.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(p.SUBTITLE_TRACK_SWITCH,this.onSubtitleTrackSwitch,this),t.on(p.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.on(p.SUBTITLE_FRAG_PROCESSED,this.onSubtitleFragProcessed,this),t.on(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(p.FRAG_BUFFERED,this.onFragBuffered,this)}_unregisterListeners(){const{hls:t}=this;t.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.LEVEL_LOADED,this.onLevelLoaded,this),t.off(p.ERROR,this.onError,this),t.off(p.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(p.SUBTITLE_TRACK_SWITCH,this.onSubtitleTrackSwitch,this),t.off(p.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.off(p.SUBTITLE_FRAG_PROCESSED,this.onSubtitleFragProcessed,this),t.off(p.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(p.FRAG_BUFFERED,this.onFragBuffered,this)}startLoad(t){this.stopLoad(),this.state=fi,this.setInterval(500),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}onManifestLoading(){this.mainDetails=null,this.fragmentTracker.removeAllFragments()}onMediaDetaching(){this.tracksBuffered=[],super.onMediaDetaching()}onLevelLoaded(t,e){this.mainDetails=e.details}onSubtitleFragProcessed(t,e){const{frag:s,success:i}=e;if(this.fragPrevious=s,this.state=fi,!i)return;const r=this.tracksBuffered[this.currentTrackId];if(!r)return;let n;const a=s.start;for(let t=0;t=r[t].start&&a<=r[t].end){n=r[t];break}const o=s.start+s.duration;n?n.end=o:(n={start:a,end:o},r.push(n)),this.fragmentTracker.fragBuffered(s),this.fragBufferedComplete(s,null)}onBufferFlushing(t,e){const{startOffset:s,endOffset:i}=e;if(0===s&&i!==Number.POSITIVE_INFINITY){const t=i-1;if(t<=0)return;e.endOffsetSubtitles=Math.max(0,t),this.tracksBuffered.forEach((e=>{for(let s=0;snew es(t))):(this.tracksBuffered=[],this.levels=e.map((t=>{const e=new es(t);return this.tracksBuffered[e.id]=[],e})),this.fragmentTracker.removeFragmentsInRange(0,Number.POSITIVE_INFINITY,_e),this.fragPrevious=null,this.mediaBuffer=null)}onSubtitleTrackSwitch(t,e){var s;if(this.currentTrackId=e.id,null==(s=this.levels)||!s.length||-1===this.currentTrackId)return void this.clearInterval();const i=this.levels[this.currentTrackId];null!=i&&i.details?this.mediaBuffer=this.mediaBufferTimeRanges:this.mediaBuffer=null,i&&this.setInterval(500)}onSubtitleTrackLoaded(t,e){var s;const{currentTrackId:i,levels:r}=this,{details:n,id:a}=e;if(!r)return void this.warn(`Subtitle tracks were reset while loading level ${a}`);const o=r[a];if(a>=r.length||!o)return;this.log(`Subtitle track ${a} loaded [${n.startSN},${n.endSN}]${n.lastPartSn?`[part-${n.lastPartSn}-${n.lastPartIndex}]`:""},duration:${n.totalduration}`),this.mediaBuffer=this.mediaBufferTimeRanges;let l=0;if(n.live||null!=(s=o.details)&&s.live){const t=this.mainDetails;if(n.deltaUpdateFailed||!t)return;const e=t.fragments[0];var h;if(o.details)l=this.alignPlaylists(n,o.details,null==(h=this.levelLastLoaded)?void 0:h.details),0===l&&e&&(l=e.start,os(n,l));else n.hasProgramDateTime&&t.hasProgramDateTime?(ei(n,t),l=n.fragments[0].start):e&&(l=e.start,os(n,l))}if(o.details=n,this.levelLastLoaded=o,a===i&&(this.startFragRequested||!this.mainDetails&&n.live||this.setStartPosition(this.mainDetails||n,l),this.tick(),n.live&&!this.fragCurrent&&this.media&&this.state===fi)){vs(null,n.fragments,this.media.currentTime,0)||(this.warn("Subtitle playlist not aligned with playback"),o.details=void 0)}}_handleFragmentLoadComplete(t){const{frag:e,payload:s}=t,i=e.decryptdata,r=this.hls;if(!this.fragContextChanged(e)&&s&&s.byteLength>0&&null!=i&&i.key&&i.iv&&"AES-128"===i.method){const t=performance.now();this.decrypter.decrypt(new Uint8Array(s),i.key.buffer,i.iv.buffer).catch((t=>{throw r.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.FRAG_DECRYPT_ERROR,fatal:!1,error:t,reason:t.message,frag:e}),t})).then((s=>{const i=performance.now();r.trigger(p.FRAG_DECRYPTED,{frag:e,payload:s,stats:{tstart:t,tdecrypt:i}})})).catch((t=>{this.warn(`${t.name}: ${t.message}`),this.state=fi}))}}doTick(){if(this.media){if(this.state===fi){const{currentTrackId:t,levels:e}=this,s=null==e?void 0:e[t];if(!s||!e.length||!s.details)return;const{config:i}=this,r=this.getLoadPosition(),n=Xs.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],r,i.maxBufferHole),{end:a,len:o}=n,l=this.getFwdBufferInfo(this.media,De),h=s.details;if(o>this.getMaxBufferLength(null==l?void 0:l.len)+h.levelTargetDuration)return;const d=h.fragments,c=d.length,u=h.edge;let f=null;const g=this.fragPrevious;if(au-t?0:t;f=vs(g,d,Math.max(d[0].start,a),e),!f&&g&&g.startthis.pollTrackChange(0),this.useTextTrackPolling=!1,this.subtitlePollingInterval=-1,this._subtitleDisplay=!0,this.onTextTracksChanged=()=>{if(this.useTextTrackPolling||self.clearInterval(this.subtitlePollingInterval),!this.media||!this.hls.config.renderTextTracksNatively)return;let t=null;const e=Ue(this.media.textTracks);for(let s=0;s-1&&this.toggleTrackModes()}registerListeners(){const{hls:t}=this;t.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.LEVEL_LOADING,this.onLevelLoading,this),t.on(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(p.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.on(p.ERROR,this.onError,this)}unregisterListeners(){const{hls:t}=this;t.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.LEVEL_LOADING,this.onLevelLoading,this),t.off(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(p.SUBTITLE_TRACK_LOADED,this.onSubtitleTrackLoaded,this),t.off(p.ERROR,this.onError,this)}onMediaAttached(t,e){this.media=e.media,this.media&&(this.queuedDefaultTrack>-1&&(this.subtitleTrack=this.queuedDefaultTrack,this.queuedDefaultTrack=-1),this.useTextTrackPolling=!(this.media.textTracks&&"onchange"in this.media.textTracks),this.useTextTrackPolling?this.pollTrackChange(500):this.media.textTracks.addEventListener("change",this.asyncPollTrackChange))}pollTrackChange(t){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.onTextTracksChanged,t)}onMediaDetaching(){if(!this.media)return;self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||this.media.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId);Ue(this.media.textTracks).forEach((t=>{Oe(t)})),this.subtitleTrack=-1,this.media=null}onManifestLoading(){this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0}onManifestParsed(t,e){this.tracks=e.subtitleTracks}onSubtitleTrackLoaded(t,e){const{id:s,groupId:i,details:r}=e,n=this.tracksInGroup[s];if(!n||n.groupId!==i)return void this.warn(`Subtitle track with id:${s} and group:${i} not found in active group ${null==n?void 0:n.groupId}`);const a=n.details;n.details=e.details,this.log(`Subtitle track ${s} "${n.name}" lang:${n.lang} group:${i} loaded [${r.startSN}-${r.endSN}]`),s===this.trackId&&this.playlistLoaded(s,e,a)}onLevelLoading(t,e){this.switchLevel(e.level)}onLevelSwitching(t,e){this.switchLevel(e.level)}switchLevel(t){const e=this.hls.levels[t];if(!e)return;const s=e.subtitleGroups||null,i=this.groupIds;let r=this.currentTrack;if(!s||(null==i?void 0:i.length)!==(null==s?void 0:s.length)||null!=s&&s.some((t=>-1===(null==i?void 0:i.indexOf(t))))){this.groupIds=s,this.trackId=-1,this.currentTrack=null;const t=this.tracks.filter((t=>!s||-1!==s.indexOf(t.groupId)));if(t.length)this.selectDefaultTrack&&!t.some((t=>t.default))&&(this.selectDefaultTrack=!1),t.forEach(((t,e)=>{t.id=e}));else if(!r&&!this.tracksInGroup.length)return;this.tracksInGroup=t;const e=this.hls.config.subtitlePreference;if(!r&&e){this.selectDefaultTrack=!1;const s=Os(e,t);if(s>-1)r=t[s];else{const t=Os(e,this.tracks);r=this.tracks[t]}}let i=this.findTrackId(r);-1===i&&r&&(i=this.findTrackId(null));const n={subtitleTracks:t};this.log(`Updating subtitle tracks, ${t.length} track(s) found in "${null==s?void 0:s.join(",")}" group-id`),this.hls.trigger(p.SUBTITLE_TRACKS_UPDATED,n),-1!==i&&-1===this.trackId&&this.setSubtitleTrack(i)}else this.shouldReloadPlaylist(r)&&this.setSubtitleTrack(this.trackId)}findTrackId(t){const e=this.tracksInGroup,s=this.selectDefaultTrack;for(let i=0;i-1){const t=this.tracksInGroup[i];return this.setSubtitleTrack(i),t}if(s)return null;{const s=Os(t,e);if(s>-1)return e[s]}}}return null}loadPlaylist(t){super.loadPlaylist();const e=this.currentTrack;if(this.shouldLoadPlaylist(e)&&e){const s=e.id,i=e.groupId;let r=e.url;if(t)try{r=t.addDirectives(r)}catch(t){this.warn(`Could not construct new URL with HLS Delivery Directives: ${t}`)}this.log(`Loading subtitle playlist for id ${s}`),this.hls.trigger(p.SUBTITLE_TRACK_LOADING,{url:r,id:s,groupId:i,deliveryDirectives:t||null})}}toggleTrackModes(){const{media:t}=this;if(!t)return;const e=Ue(t.textTracks),s=this.currentTrack;let i;if(s&&(i=e.filter((t=>Mr(s,t)))[0],i||this.warn(`Unable to find subtitle TextTrack with name "${s.name}" and language "${s.lang}"`)),[].slice.call(e).forEach((t=>{"disabled"!==t.mode&&t!==i&&(t.mode="disabled")})),i){const t=this.subtitleDisplay?"showing":"hidden";i.mode!==t&&(i.mode=t)}}setSubtitleTrack(t){const e=this.tracksInGroup;if(!this.media)return void(this.queuedDefaultTrack=t);if(t<-1||t>=e.length||!f(t))return void this.warn(`Invalid subtitle track id: ${t}`);this.clearTimer(),this.selectDefaultTrack=!1;const s=this.currentTrack,i=e[t]||null;if(this.trackId=t,this.currentTrack=i,this.toggleTrackModes(),!i)return void this.hls.trigger(p.SUBTITLE_TRACK_SWITCH,{id:t});const r=!!i.details&&!i.details.live;if(t===this.trackId&&i===s&&r)return;this.log(`Switching to subtitle-track ${t}`+(i?` "${i.name}" lang:${i.lang} group:${i.groupId}`:""));const{id:n,groupId:a="",name:o,type:l,url:h}=i;this.hls.trigger(p.SUBTITLE_TRACK_SWITCH,{id:n,groupId:a,name:o,type:l,url:h});const d=this.switchParams(i.url,null==s?void 0:s.details,i.details);this.loadPlaylist(d)}},timelineController:class{constructor(t){this.hls=void 0,this.media=null,this.config=void 0,this.enabled=!0,this.Cues=void 0,this.textTracks=[],this.tracks=[],this.initPTS=[],this.unparsedVttFrags=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.cea608Parser1=void 0,this.cea608Parser2=void 0,this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this.captionsProperties=void 0,this.hls=t,this.config=t.config,this.Cues=t.config.cueHandler,this.captionsProperties={textTrack1:{label:this.config.captionsTextTrack1Label,languageCode:this.config.captionsTextTrack1LanguageCode},textTrack2:{label:this.config.captionsTextTrack2Label,languageCode:this.config.captionsTextTrack2LanguageCode},textTrack3:{label:this.config.captionsTextTrack3Label,languageCode:this.config.captionsTextTrack3LanguageCode},textTrack4:{label:this.config.captionsTextTrack4Label,languageCode:this.config.captionsTextTrack4LanguageCode}},t.on(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(p.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(p.FRAG_LOADING,this.onFragLoading,this),t.on(p.FRAG_LOADED,this.onFragLoaded,this),t.on(p.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.on(p.FRAG_DECRYPTED,this.onFragDecrypted,this),t.on(p.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(p.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.on(p.BUFFER_FLUSHING,this.onBufferFlushing,this)}destroy(){const{hls:t}=this;t.off(p.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(p.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(p.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(p.FRAG_LOADING,this.onFragLoading,this),t.off(p.FRAG_LOADED,this.onFragLoaded,this),t.off(p.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.off(p.FRAG_DECRYPTED,this.onFragDecrypted,this),t.off(p.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(p.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.off(p.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=null,this.cea608Parser1=this.cea608Parser2=void 0}initCea608Parsers(){if(this.config.enableCEA708Captions&&(!this.cea608Parser1||!this.cea608Parser2)){const t=new nn(this,"textTrack1"),e=new nn(this,"textTrack2"),s=new nn(this,"textTrack3"),i=new nn(this,"textTrack4");this.cea608Parser1=new en(1,t,e),this.cea608Parser2=new en(3,s,i)}}addCues(t,e,s,i,r){let n=!1;for(let t=r.length;t--;){const i=r[t],d=(a=i[0],o=i[1],l=e,h=s,Math.min(o,h)-Math.max(a,l));if(d>=0&&(i[0]=Math.min(i[0],e),i[1]=Math.max(i[1],s),n=!0,d/(s-e)>.5))return}var a,o,l,h;if(n||r.push([e,s]),this.config.renderTextTracksNatively){const r=this.captionsTracks[t];this.Cues.newCue(r,e,s,i)}else{const r=this.Cues.newCue(null,e,s,i);this.hls.trigger(p.CUES_PARSED,{type:"captions",cues:r,track:t})}}onInitPtsFound(t,{frag:e,id:s,initPTS:i,timescale:r}){const{unparsedVttFrags:n}=this;"main"===s&&(this.initPTS[e.cc]={baseTime:i,timescale:r}),n.length&&(this.unparsedVttFrags=[],n.forEach((t=>{this.onFragLoaded(p.FRAG_LOADED,t)})))}getExistingTrack(t,e){const{media:s}=this;if(s)for(let i=0;i{Oe(t[e]),delete t[e]})),this.nonNativeCaptionsTracks={}}onManifestLoading(){this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this._cleanTracks(),this.tracks=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.textTracks=[],this.unparsedVttFrags=[],this.initPTS=[],this.cea608Parser1&&this.cea608Parser2&&(this.cea608Parser1.reset(),this.cea608Parser2.reset())}_cleanTracks(){const{media:t}=this;if(!t)return;const e=t.textTracks;if(e)for(let t=0;tt.textCodec===Sn));if(this.config.enableWebVTT||i&&this.config.enableIMSC1){if(xr(this.tracks,s))return void(this.tracks=s);if(this.textTracks=[],this.tracks=s,this.config.renderTextTracksNatively){const t=this.media,e=t?Ue(t.textTracks):null;if(this.tracks.forEach(((t,s)=>{let i;if(e){let s=null;for(let i=0;inull!==t)).map((t=>t.label));t.length&&A.warn(`Media element contains unused subtitle tracks: ${t.join(", ")}. Replace media element for each source to clear TextTracks and captions menu.`)}}else if(this.tracks.length){const t=this.tracks.map((t=>({label:t.name,kind:t.type.toLowerCase(),default:t.default,subtitleTrack:t})));this.hls.trigger(p.NON_NATIVE_TEXT_TRACKS_FOUND,{tracks:t})}}}onManifestLoaded(t,e){this.config.enableCEA708Captions&&e.captions&&e.captions.forEach((t=>{const e=/(?:CC|SERVICE)([1-4])/.exec(t.instreamId);if(!e)return;const s=`textTrack${e[1]}`,i=this.captionsProperties[s];i&&(i.label=t.name,t.lang&&(i.languageCode=t.lang),i.media=t)}))}closedCaptionsForLevel(t){const e=this.hls.levels[t.level];return null==e?void 0:e.attrs["CLOSED-CAPTIONS"]}onFragLoading(t,e){if(this.enabled&&e.frag.type===De){var s,i;const{cea608Parser1:t,cea608Parser2:r,lastSn:n}=this,{cc:a,sn:o}=e.frag,l=null!=(s=null==(i=e.part)?void 0:i.index)?s:-1;t&&r&&(o!==n+1||o===n&&l!==this.lastPartIndex+1||a!==this.lastCc)&&(t.reset(),r.reset()),this.lastCc=a,this.lastSn=o,this.lastPartIndex=l}}onFragLoaded(t,e){const{frag:s,payload:i}=e;if(s.type===_e)if(i.byteLength){const t=s.decryptdata,r="stats"in e;if(null==t||!t.encrypted||r){const t=this.tracks[s.level],r=this.vttCCs;r[s.cc]||(r[s.cc]={start:s.start,prevCC:this.prevCC,new:!0},this.prevCC=s.cc),t&&t.textCodec===Sn?this._parseIMSC1(s,i):this._parseVTTs(e)}}else this.hls.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:s,error:new Error("Empty subtitle payload")})}_parseIMSC1(t,e){const s=this.hls;bn(e,this.initPTS[t.cc],(e=>{this._appendCues(e,t.level),s.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!0,frag:t})}),(e=>{A.log(`Failed to parse IMSC1: ${e}`),s.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:t,error:e})}))}_parseVTTs(t){var e;const{frag:s,payload:i}=t,{initPTS:r,unparsedVttFrags:n}=this,a=r.length-1;if(!r[s.cc]&&-1===a)return void n.push(t);const o=this.hls;Tn(null!=(e=s.initSegment)&&e.data?Bt(s.initSegment.data,new Uint8Array(i)):i,this.initPTS[s.cc],this.vttCCs,s.cc,s.start,(t=>{this._appendCues(t,s.level),o.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!0,frag:s})}),(e=>{const r="Missing initPTS for VTT MPEGTS"===e.message;r?n.push(t):this._fallbackToIMSC1(s,i),A.log(`Failed to parse VTT cue: ${e}`),r&&a>s.cc||o.trigger(p.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:s,error:e})}))}_fallbackToIMSC1(t,e){const s=this.tracks[t.level];s.textCodec||bn(e,this.initPTS[t.cc],(()=>{s.textCodec=Sn,this._parseIMSC1(t,e)}),(()=>{s.textCodec="wvtt"}))}_appendCues(t,e){const s=this.hls;if(this.config.renderTextTracksNatively){const s=this.textTracks[e];if(!s||"disabled"===s.mode)return;t.forEach((t=>Fe(s,t)))}else{const i=this.tracks[e];if(!i)return;const r=i.default?"default":"subtitles"+e;s.trigger(p.CUES_PARSED,{type:"subtitles",cues:t,track:r})}}onFragDecrypted(t,e){const{frag:s}=e;s.type===_e&&this.onFragLoaded(p.FRAG_LOADED,e)}onSubtitleTracksCleared(){this.tracks=[],this.captionsTracks={}}onFragParsingUserdata(t,e){this.initCea608Parsers();const{cea608Parser1:s,cea608Parser2:i}=this;if(!this.enabled||!s||!i)return;const{frag:r,samples:n}=e;if(r.type!==De||"NONE"!==this.closedCaptionsForLevel(r))for(let t=0;tNe(t[i],e,s)))}if(this.config.renderTextTracksNatively&&0===e&&void 0!==i){const{textTracks:t}=this;Object.keys(t).forEach((s=>Ne(t[s],e,i)))}}}extractCea608Data(t){const e=[[],[]],s=31&t[0];let i=2;for(let r=0;r0&&-1===t?(this.log(`Override startPosition with lastCurrentTime @${e.toFixed(3)}`),t=e,this.state=fi):(this.loadedmetadata=!1,this.state=vi),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}doTick(){switch(this.state){case fi:this.doTickIdle();break;case vi:{var t;const{levels:e,trackId:s}=this,i=null==e||null==(t=e[s])?void 0:t.details;if(i){if(this.waitForCdnTuneIn(i))break;this.state=Li}break}case pi:{var e;const t=performance.now(),s=this.retryDate;if(!s||t>=s||null!=(e=this.media)&&e.seeking){const{levels:t,trackId:e}=this;this.log("RetryDate reached, switch back to IDLE state"),this.resetStartWhenNotLoaded((null==t?void 0:t[e])||null),this.state=fi}break}case Li:{const t=this.waitingData;if(t){const{frag:e,part:s,cache:i,complete:r}=t;if(void 0!==this.initPTS[e.cc]){this.waitingData=null,this.waitingVideoCC=-1,this.state=mi;const t={frag:e,part:s,payload:i.flush(),networkDetails:null};this._handleFragmentLoadProgress(t),r&&super._handleFragmentLoadComplete(t)}else if(this.videoTrackCC!==this.waitingVideoCC)this.log(`Waiting fragment cc (${e.cc}) cancelled because video is at cc ${this.videoTrackCC}`),this.clearWaitingFragment();else{const t=this.getLoadPosition(),s=Xs.bufferInfo(this.mediaBuffer,t,this.config.maxBufferHole);ys(s.end,this.config.maxFragLookUpTolerance,e)<0&&(this.log(`Waiting fragment cc (${e.cc}) @ ${e.start} cancelled because another fragment at ${s.end} is needed`),this.clearWaitingFragment())}}else this.state=fi}}this.onTickEnd()}clearWaitingFragment(){const t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null,this.waitingVideoCC=-1,this.state=fi)}resetLoadingState(){this.clearWaitingFragment(),super.resetLoadingState()}onTickEnd(){const{media:t}=this;null!=t&&t.readyState&&(this.lastCurrentTime=t.currentTime)}doTickIdle(){const{hls:t,levels:e,media:s,trackId:i}=this,r=t.config;if(!s&&(this.startFragRequested||!r.startFragPrefetch)||null==e||!e[i])return;const n=e[i],a=n.details;if(!a||a.live&&this.levelLastLoaded!==n||this.waitForCdnTuneIn(a))return void(this.state=vi);const o=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&o&&(this.bufferFlushed=!1,this.afterBufferFlushed(o,_,Ie));const l=this.getFwdBufferInfo(o,Ie);if(null===l)return;const{bufferedTrack:h,switchingTrack:d}=this;if(!d&&this._streamEnded(l,a))return t.trigger(p.BUFFER_EOS,{type:"audio"}),void(this.state=Ti);const c=this.getFwdBufferInfo(this.videoBuffer?this.videoBuffer:this.media,De),u=l.len,f=this.getMaxBufferLength(null==c?void 0:c.len),g=a.fragments,m=g[0].start;let v=this.flushing?this.getLoadPosition():l.end;if(d&&s){const t=this.getLoadPosition();h&&!Pr(d.attrs,h.attrs)&&(v=t),a.PTSKnown&&tm||l.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),s.currentTime=m+.05)}if(u>=f&&!d&&vc.end+a.targetduration;if(T||(null==c||!c.len)&&l.len){const t=this.getAppendedFrag(y.start,De);if(null===t)return;if(E||(E=!!t.gap||!!T&&0===c.len),T&&!E||E&&l.nextStart&&l.nextStartnew es(t)))}onAudioTrackSwitching(t,e){const s=!!e.url;this.trackId=e.id;const{fragCurrent:i}=this;i&&(i.abortRequests(),this.removeUnbufferedFrags(i.start)),this.resetLoadingState(),s?this.setInterval(100):this.resetTransmuxer(),s?(this.switchingTrack=e,this.state=fi,this.flushAudioIfNeeded(e)):(this.switchingTrack=null,this.bufferedTrack=e,this.state=ui),this.tick()}onManifestLoading(){this.fragmentTracker.removeAllFragments(),this.startPosition=this.lastCurrentTime=0,this.bufferFlushed=this.flushing=!1,this.levels=this.mainDetails=this.waitingData=this.bufferedTrack=this.cachedTrackLoadedData=this.switchingTrack=null,this.startFragRequested=!1,this.trackId=this.videoTrackCC=this.waitingVideoCC=-1}onLevelLoaded(t,e){this.mainDetails=e.details,null!==this.cachedTrackLoadedData&&(this.hls.trigger(p.AUDIO_TRACK_LOADED,this.cachedTrackLoadedData),this.cachedTrackLoadedData=null)}onAudioTrackLoaded(t,e){var s;if(null==this.mainDetails)return void(this.cachedTrackLoadedData=e);const{levels:i}=this,{details:r,id:n}=e;if(!i)return void this.warn(`Audio tracks were reset while loading level ${n}`);this.log(`Audio track ${n} loaded [${r.startSN},${r.endSN}]${r.lastPartSn?`[part-${r.lastPartSn}-${r.lastPartIndex}]`:""},duration:${r.totalduration}`);const a=i[n];let o=0;if(r.live||null!=(s=a.details)&&s.live){this.checkLiveUpdate(r);const t=this.mainDetails;if(r.deltaUpdateFailed||!t)return;var l;if(!a.details&&r.hasProgramDateTime&&t.hasProgramDateTime)ei(r,t),o=r.fragments[0].start;else o=this.alignPlaylists(r,a.details,null==(l=this.levelLastLoaded)?void 0:l.details)}a.details=r,this.levelLastLoaded=a,this.startFragRequested||!this.mainDetails&&r.live||this.setStartPosition(this.mainDetails||r,o),this.state!==vi||this.waitForCdnTuneIn(r)||(this.state=fi),this.tick()}_handleFragmentLoadProgress(t){var e;const{frag:s,part:i,payload:r}=t,{config:n,trackId:a,levels:o}=this;if(!o)return void this.warn(`Audio tracks were reset while fragment load was in progress. Fragment ${s.sn} of level ${s.level} will not be buffered`);const l=o[a];if(!l)return void this.warn("Audio track is undefined on fragment load progress");const h=l.details;if(!h)return this.warn("Audio track details undefined on fragment load progress"),void this.removeUnbufferedFrags(s.start);const d=n.defaultAudioCodec||l.audioCodec||"mp4a.40.2";let c=this.transmuxer;c||(c=this.transmuxer=new Cr(this.hls,Ie,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)));const u=this.initPTS[s.cc],f=null==(e=s.initSegment)?void 0:e.data;if(void 0!==u){const t=!1,e=i?i.index:-1,n=-1!==e,a=new zs(s.level,s.sn,s.stats.chunkCount,r.byteLength,e,n);c.push(r,f,d,"",s,i,h.totalduration,t,a,u)}else{this.log(`Unknown video PTS for cc ${s.cc}, waiting for video PTS before demuxing audio frag ${s.sn} of [${h.startSN} ,${h.endSN}],track ${a}`);const{cache:t}=this.waitingData=this.waitingData||{frag:s,part:i,cache:new bi,complete:!1};t.push(new Uint8Array(r)),this.waitingVideoCC=this.videoTrackCC,this.state=Li}}_handleFragmentLoadComplete(t){this.waitingData?this.waitingData.complete=!0:super._handleFragmentLoadComplete(t)}onBufferReset(){this.mediaBuffer=this.videoBuffer=null,this.loadedmetadata=!1}onBufferCreated(t,e){const s=e.tracks.audio;s&&(this.mediaBuffer=s.buffer||null),e.tracks.video&&(this.videoBuffer=e.tracks.video.buffer||null)}onFragBuffered(t,e){const{frag:s,part:i}=e;if(s.type===Ie)if(this.fragContextChanged(s))this.warn(`Fragment ${s.sn}${i?" p: "+i.index:""} of level ${s.level} finished buffering, but was aborted. state: ${this.state}, audioSwitch: ${this.switchingTrack?this.switchingTrack.name:"false"}`);else{if("initSegment"!==s.sn){this.fragPrevious=s;const t=this.switchingTrack;t&&(this.bufferedTrack=t,this.switchingTrack=null,this.hls.trigger(p.AUDIO_TRACK_SWITCHED,h({},t)))}this.fragBufferedComplete(s,i)}else if(!this.loadedmetadata&&s.type===De){const t=this.videoBuffer||this.media;if(t){Xs.getBuffered(t).length&&(this.loadedmetadata=!0)}}}onError(t,e){var s;if(e.fatal)this.state=Si;else switch(e.details){case y.FRAG_GAP:case y.FRAG_PARSING_ERROR:case y.FRAG_DECRYPT_ERROR:case y.FRAG_LOAD_ERROR:case y.FRAG_LOAD_TIMEOUT:case y.KEY_LOAD_ERROR:case y.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(Ie,e);break;case y.AUDIO_TRACK_LOAD_ERROR:case y.AUDIO_TRACK_LOAD_TIMEOUT:case y.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==vi||(null==(s=e.context)?void 0:s.type)!==ke||(this.state=fi);break;case y.BUFFER_APPEND_ERROR:case y.BUFFER_FULL_ERROR:if(!e.parent||"audio"!==e.parent)return;if(e.details===y.BUFFER_APPEND_ERROR)return void this.resetLoadingState();this.reduceLengthAndFlushBuffer(e)&&(this.bufferedTrack=null,super.flushMainBuffer(0,Number.POSITIVE_INFINITY,"audio"));break;case y.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}}onBufferFlushing(t,{type:e}){e!==C&&(this.flushing=!0)}onBufferFlushed(t,{type:e}){if(e!==C){this.flushing=!1,this.bufferFlushed=!0,this.state===Ti&&(this.state=fi);const t=this.mediaBuffer||this.media;t&&(this.afterBufferFlushed(t,e,Ie),this.tick())}}_handleTransmuxComplete(t){var e;const s="audio",{hls:i}=this,{remuxResult:r,chunkMeta:n}=t,a=this.getCurrentContext(n);if(!a)return void this.resetWhenMissingContext(n);const{frag:o,part:l,level:h}=a,{details:d}=h,{audio:c,text:f,id3:g,initSegment:m}=r;if(!this.fragContextChanged(o)&&d){if(this.state=yi,this.switchingTrack&&c&&this.completeAudioSwitch(this.switchingTrack),null!=m&&m.tracks){const t=o.initSegment||o;this._bufferInitSegment(h,m.tracks,t,n),i.trigger(p.FRAG_PARSING_INIT_SEGMENT,{frag:t,id:s,tracks:m.tracks})}if(c){const{startPTS:t,endPTS:e,startDTS:s,endDTS:i}=c;l&&(l.elementaryStreams[_]={startPTS:t,endPTS:e,startDTS:s,endDTS:i}),o.setElementaryStreamInfo(_,t,e,s,i),this.bufferFragmentData(c,o,l,n)}if(null!=g&&null!=(e=g.samples)&&e.length){const t=u({id:s,frag:o,details:d},g);i.trigger(p.FRAG_PARSING_METADATA,t)}if(f){const t=u({id:s,frag:o,details:d},f);i.trigger(p.FRAG_PARSING_USERDATA,t)}}else this.fragmentTracker.removeFragment(o)}_bufferInitSegment(t,e,s,i){if(this.state!==yi)return;e.video&&delete e.video;const r=e.audio;if(!r)return;r.id="audio";const n=t.audioCodec;this.log(`Init audio buffer, container:${r.container}, codecs[level/parsed]=[${n}/${r.codec}]`),n&&1===n.split(",").length&&(r.levelCodec=n),this.hls.trigger(p.BUFFER_CODECS,e);const a=r.initSegment;if(null!=a&&a.byteLength){const t={type:"audio",frag:s,part:null,chunkMeta:i,parent:s.type,data:a};this.hls.trigger(p.BUFFER_APPENDING,t)}this.tickImmediate()}loadFragment(t,e,s){const i=this.fragmentTracker.getState(t);var r;if(this.fragCurrent=t,this.switchingTrack||i===Gs||i===Hs)if("initSegment"===t.sn)this._loadInitSegment(t,e);else if(null!=(r=e.details)&&r.live&&!this.initPTS[t.cc]){this.log(`Waiting for video PTS in continuity counter ${t.cc} of live stream before loading audio fragment ${t.sn} of level ${this.trackId}`),this.state=Li;const s=this.mainDetails;s&&s.fragments[0].start!==e.details.fragments[0].start&&ei(e.details,s)}else this.startFragRequested=!0,super.loadFragment(t,e,s);else this.clearTrackerIfNeeded(t)}flushAudioIfNeeded(t){const{media:e,bufferedTrack:s}=this,i=null==s?void 0:s.attrs,r=t.attrs;e&&i&&(i.CHANNELS!==r.CHANNELS||s.name!==t.name||s.lang!==t.lang)&&(this.log("Switching audio track : flushing all audio"),super.flushMainBuffer(0,Number.POSITIVE_INFINITY,"audio"),this.bufferedTrack=null)}completeAudioSwitch(t){const{hls:e}=this;this.flushAudioIfNeeded(t),this.bufferedTrack=t,this.switchingTrack=null,e.trigger(p.AUDIO_TRACK_SWITCHED,h({},t))}},audioTrackController:class extends ws{constructor(t){super(t,"[audio-track-controller]"),this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0,this.registerListeners()}registerListeners(){const{hls:t}=this;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.LEVEL_LOADING,this.onLevelLoading,this),t.on(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(p.AUDIO_TRACK_LOADED,this.onAudioTrackLoaded,this),t.on(p.ERROR,this.onError,this)}unregisterListeners(){const{hls:t}=this;t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.LEVEL_LOADING,this.onLevelLoading,this),t.off(p.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(p.AUDIO_TRACK_LOADED,this.onAudioTrackLoaded,this),t.off(p.ERROR,this.onError,this)}destroy(){this.unregisterListeners(),this.tracks.length=0,this.tracksInGroup.length=0,this.currentTrack=null,super.destroy()}onManifestLoading(){this.tracks=[],this.tracksInGroup=[],this.groupIds=null,this.currentTrack=null,this.trackId=-1,this.selectDefaultTrack=!0}onManifestParsed(t,e){this.tracks=e.audioTracks||[]}onAudioTrackLoaded(t,e){const{id:s,groupId:i,details:r}=e,n=this.tracksInGroup[s];if(!n||n.groupId!==i)return void this.warn(`Audio track with id:${s} and group:${i} not found in active group ${null==n?void 0:n.groupId}`);const a=n.details;n.details=e.details,this.log(`Audio track ${s} "${n.name}" lang:${n.lang} group:${i} loaded [${r.startSN}-${r.endSN}]`),s===this.trackId&&this.playlistLoaded(s,e,a)}onLevelLoading(t,e){this.switchLevel(e.level)}onLevelSwitching(t,e){this.switchLevel(e.level)}switchLevel(t){const e=this.hls.levels[t];if(!e)return;const s=e.audioGroups||null,i=this.groupIds;let r=this.currentTrack;if(!s||(null==i?void 0:i.length)!==(null==s?void 0:s.length)||null!=s&&s.some((t=>-1===(null==i?void 0:i.indexOf(t))))){this.groupIds=s,this.trackId=-1,this.currentTrack=null;const t=this.tracks.filter((t=>!s||-1!==s.indexOf(t.groupId)));if(t.length)this.selectDefaultTrack&&!t.some((t=>t.default))&&(this.selectDefaultTrack=!1),t.forEach(((t,e)=>{t.id=e}));else if(!r&&!this.tracksInGroup.length)return;this.tracksInGroup=t;const e=this.hls.config.audioPreference;if(!r&&e){const s=Os(e,t,Us);if(s>-1)r=t[s];else{const t=Os(e,this.tracks);r=this.tracks[t]}}let i=this.findTrackId(r);-1===i&&r&&(i=this.findTrackId(null));const a={audioTracks:t};this.log(`Updating audio tracks, ${t.length} track(s) found in group(s): ${null==s?void 0:s.join(",")}`),this.hls.trigger(p.AUDIO_TRACKS_UPDATED,a);const o=this.trackId;if(-1!==i&&-1===o)this.setAudioTrack(i);else if(t.length&&-1===o){var n;const e=new Error(`No audio track selected for current audio group-ID(s): ${null==(n=this.groupIds)?void 0:n.join(",")} track count: ${t.length}`);this.warn(e.message),this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:e})}}else this.shouldReloadPlaylist(r)&&this.setAudioTrack(this.trackId)}onError(t,e){!e.fatal&&e.context&&(e.context.type!==ke||e.context.id!==this.trackId||this.groupIds&&-1===this.groupIds.indexOf(e.context.groupId)||(this.requestScheduled=-1,this.checkRetry(e)))}get allAudioTracks(){return this.tracks}get audioTracks(){return this.tracksInGroup}get audioTrack(){return this.trackId}set audioTrack(t){this.selectDefaultTrack=!1,this.setAudioTrack(t)}setAudioOption(t){const e=this.hls;if(e.config.audioPreference=t,t){const s=this.allAudioTracks;if(this.selectDefaultTrack=!1,s.length){const i=this.currentTrack;if(i&&Ns(t,i,Us))return i;const r=Os(t,this.tracksInGroup,Us);if(r>-1){const t=this.tracksInGroup[r];return this.setAudioTrack(r),t}if(i){let i=e.loadLevel;-1===i&&(i=e.firstAutoLevel);const r=function(t,e,s,i,r){const n=e[i],a=e.reduce(((t,e,s)=>{const i=e.uri;return(t[i]||(t[i]=[])).push(s),t}),{})[n.uri];a.length>1&&(i=Math.max.apply(Math,a));const o=n.videoRange,l=n.frameRate,h=n.codecSet.substring(0,4),d=Bs(e,i,(e=>{if(e.videoRange!==o||e.frameRate!==l||e.codecSet.substring(0,4)!==h)return!1;const i=e.audioGroups,n=s.filter((t=>!i||-1!==i.indexOf(t.groupId)));return Os(t,n,r)>-1}));return d>-1?d:Bs(e,i,(e=>{const i=e.audioGroups,n=s.filter((t=>!i||-1!==i.indexOf(t.groupId)));return Os(t,n,r)>-1}))}(t,e.levels,s,i,Us);if(-1===r)return null;e.nextLoadLevel=r}if(t.channels||t.audioCodec){const e=Os(t,s);if(e>-1)return s[e]}}}return null}setAudioTrack(t){const e=this.tracksInGroup;if(t<0||t>=e.length)return void this.warn(`Invalid audio track id: ${t}`);this.clearTimer(),this.selectDefaultTrack=!1;const s=this.currentTrack,i=e[t],r=i.details&&!i.details.live;if(t===this.trackId&&i===s&&r)return;if(this.log(`Switching to audio-track ${t} "${i.name}" lang:${i.lang} group:${i.groupId} channels:${i.channels}`),this.trackId=t,this.currentTrack=i,this.hls.trigger(p.AUDIO_TRACK_SWITCHING,h({},i)),r)return;const n=this.switchParams(i.url,null==s?void 0:s.details,i.details);this.loadPlaylist(n)}findTrackId(t){const e=this.tracksInGroup;for(let s=0;s{this.initialized&&(this.starved=!0),this.buffering=!0},this.onPlaying=()=>{this.initialized||(this.initialized=!0),this.buffering=!1},this.applyPlaylistData=t=>{try{this.apply(t,{ot:Un.MANIFEST,su:!this.initialized})}catch(t){A.warn("Could not generate manifest CMCD data.",t)}},this.applyFragmentData=t=>{try{const e=t.frag,s=this.hls.levels[e.level],i=this.getObjectType(e),r={d:1e3*e.duration,ot:i};i!==Un.VIDEO&&i!==Un.AUDIO&&i!=Un.MUXED||(r.br=s.bitrate/1e3,r.tb=this.getTopBandwidth(i)/1e3,r.bl=this.getBufferLength(i)),this.apply(t,r)}catch(t){A.warn("Could not generate segment CMCD data.",t)}},this.hls=t;const e=this.config=t.config,{cmcd:s}=e;null!=s&&(e.pLoader=this.createPlaylistLoader(),e.fLoader=this.createFragmentLoader(),this.sid=s.sessionId||function(){try{return crypto.randomUUID()}catch(t){try{const t=URL.createObjectURL(new Blob),e=t.toString();return URL.revokeObjectURL(t),e.slice(e.lastIndexOf("/")+1)}catch(t){let e=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(t=>{const s=(e+16*Math.random())%16|0;return e=Math.floor(e/16),("x"==t?s:3&s|8).toString(16)}))}}}(),this.cid=s.contentId,this.useHeaders=!0===s.useHeaders,this.includeKeys=s.includeKeys,this.registerListeners())}registerListeners(){const t=this.hls;t.on(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(p.MEDIA_DETACHED,this.onMediaDetached,this),t.on(p.BUFFER_CREATED,this.onBufferCreated,this)}unregisterListeners(){const t=this.hls;t.off(p.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(p.MEDIA_DETACHED,this.onMediaDetached,this),t.off(p.BUFFER_CREATED,this.onBufferCreated,this)}destroy(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null,this.onWaiting=this.onPlaying=null}onMediaAttached(t,e){this.media=e.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)}onMediaDetached(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)}onBufferCreated(t,e){var s,i;this.audioBuffer=null==(s=e.tracks.audio)?void 0:s.buffer,this.videoBuffer=null==(i=e.tracks.video)?void 0:i.buffer}createData(){var t;return{v:1,sf:Bn.HLS,sid:this.sid,cid:this.cid,pr:null==(t=this.media)?void 0:t.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}}apply(t,e={}){u(e,this.createData());const s=e.ot===Un.INIT||e.ot===Un.VIDEO||e.ot===Un.MUXED;this.starved&&s&&(e.bs=!0,e.su=!0,this.starved=!1),null==e.su&&(e.su=this.buffering);const{includeKeys:i}=this;i&&(e=Object.keys(e).reduce(((t,s)=>(i.includes(s)&&(t[s]=e[s]),t)),{})),this.useHeaders?(t.headers||(t.headers={}),ha(t.headers,e)):t.url=ca(t.url,e)}getObjectType(t){const{type:e}=t;return"subtitle"===e?Un.TIMED_TEXT:"initSegment"===t.sn?Un.INIT:"audio"===e?Un.AUDIO:"main"===e?this.hls.audioTracks.length?Un.VIDEO:Un.MUXED:void 0}getTopBandwidth(t){let e,s=0;const i=this.hls;if(t===Un.AUDIO)e=i.audioTracks;else{const t=i.maxAutoLevel,s=t>-1?t+1:i.levels.length;e=i.levels.slice(0,s)}for(const t of e)t.bitrate>s&&(s=t.bitrate);return s>0?s:NaN}getBufferLength(t){const e=this.hls.media,s=t===Un.AUDIO?this.audioBuffer:this.videoBuffer;if(!s||!e)return NaN;return 1e3*Xs.bufferInfo(s,e.currentTime,this.config.maxBufferHole).len}createPlaylistLoader(){const{pLoader:t}=this.config,e=this.applyPlaylistData,s=t||this.config.loader;return class{constructor(t){this.loader=void 0,this.loader=new s(t)}get stats(){return this.loader.stats}get context(){return this.loader.context}destroy(){this.loader.destroy()}abort(){this.loader.abort()}load(t,s,i){e(t),this.loader.load(t,s,i)}}}createFragmentLoader(){const{fLoader:t}=this.config,e=this.applyFragmentData,s=t||this.config.loader;return class{constructor(t){this.loader=void 0,this.loader=new s(t)}get stats(){return this.loader.stats}get context(){return this.loader.context}destroy(){this.loader.destroy()}abort(){this.loader.abort()}load(t,s,i){e(t),this.loader.load(t,s,i)}}}},contentSteeringController:class{constructor(t){this.hls=void 0,this.log=void 0,this.loader=null,this.uri=null,this.pathwayId=".",this.pathwayPriority=null,this.timeToLoad=300,this.reloadTimer=-1,this.updated=0,this.started=!1,this.enabled=!0,this.levels=null,this.audioTracks=null,this.subtitleTracks=null,this.penalizedPathways={},this.hls=t,this.log=A.log.bind(A,"[content-steering]:"),this.registerListeners()}registerListeners(){const t=this.hls;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(p.MANIFEST_PARSED,this.onManifestParsed,this),t.on(p.ERROR,this.onError,this)}unregisterListeners(){const t=this.hls;t&&(t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(p.MANIFEST_PARSED,this.onManifestParsed,this),t.off(p.ERROR,this.onError,this))}startLoad(){if(this.started=!0,this.clearTimeout(),this.enabled&&this.uri){if(this.updated){const t=1e3*this.timeToLoad-(performance.now()-this.updated);if(t>0)return void this.scheduleRefresh(this.uri,t)}this.loadSteeringManifest(this.uri)}}stopLoad(){this.started=!1,this.loader&&(this.loader.destroy(),this.loader=null),this.clearTimeout()}clearTimeout(){-1!==this.reloadTimer&&(self.clearTimeout(this.reloadTimer),this.reloadTimer=-1)}destroy(){this.unregisterListeners(),this.stopLoad(),this.hls=null,this.levels=this.audioTracks=this.subtitleTracks=null}removeLevel(t){const e=this.levels;e&&(this.levels=e.filter((e=>e!==t)))}onManifestLoading(){this.stopLoad(),this.enabled=!0,this.timeToLoad=300,this.updated=0,this.uri=null,this.pathwayId=".",this.levels=this.audioTracks=this.subtitleTracks=null}onManifestLoaded(t,e){const{contentSteering:s}=e;null!==s&&(this.pathwayId=s.pathwayId,this.uri=s.uri,this.started&&this.startLoad())}onManifestParsed(t,e){this.audioTracks=e.audioTracks,this.subtitleTracks=e.subtitleTracks}onError(t,e){const{errorAction:s}=e;if((null==s?void 0:s.action)===Ss&&s.flags===bs){const t=this.levels;let i=this.pathwayPriority,r=this.pathwayId;if(e.context){const{groupId:s,pathwayId:i,type:n}=e.context;s&&t?r=this.getPathwayForGroupId(s,n,r):i&&(r=i)}r in this.penalizedPathways||(this.penalizedPathways[r]=performance.now()),!i&&t&&(i=t.reduce(((t,e)=>(-1===t.indexOf(e.pathwayId)&&t.push(e.pathwayId),t)),[])),i&&i.length>1&&(this.updatePathwayPriority(i),s.resolved=this.pathwayId!==r),s.resolved||A.warn(`Could not resolve ${e.details} ("${e.error.message}") with content-steering for Pathway: ${r} levels: ${t?t.length:t} priorities: ${JSON.stringify(i)} penalized: ${JSON.stringify(this.penalizedPathways)}`)}}filterParsedLevels(t){this.levels=t;let e=this.getLevelsForPathway(this.pathwayId);if(0===e.length){const s=t[0].pathwayId;this.log(`No levels found in Pathway ${this.pathwayId}. Setting initial Pathway to "${s}"`),e=this.getLevelsForPathway(s),this.pathwayId=s}return e.length!==t.length?(this.log(`Found ${e.length}/${t.length} levels in Pathway "${this.pathwayId}"`),e):t}getLevelsForPathway(t){return null===this.levels?[]:this.levels.filter((e=>t===e.pathwayId))}updatePathwayPriority(t){let e;this.pathwayPriority=t;const s=this.penalizedPathways,i=performance.now();Object.keys(s).forEach((t=>{i-s[t]>3e5&&delete s[t]}));for(let i=0;i0){this.log(`Setting Pathway to "${r}"`),this.pathwayId=r,ds(e),this.hls.trigger(p.LEVELS_UPDATED,{levels:e});const t=this.hls.levels[n];a&&t&&this.levels&&(t.attrs["STABLE-VARIANT-ID"]!==a.attrs["STABLE-VARIANT-ID"]&&t.bitrate!==a.bitrate&&this.log(`Unstable Pathways change from bitrate ${a.bitrate} to ${t.bitrate}`),this.hls.nextLoadLevel=n);break}}}getPathwayForGroupId(t,e,s){const i=this.getLevelsForPathway(s).concat(this.levels||[]);for(let s=0;s{const{ID:r,"BASE-ID":n,"URI-REPLACEMENT":a}=t;if(e.some((t=>t.pathwayId===r)))return;const o=this.getLevelsForPathway(n).map((t=>{const e=new k(t.attrs);e["PATHWAY-ID"]=r;const n=e.AUDIO&&`${e.AUDIO}_clone_${r}`,o=e.SUBTITLES&&`${e.SUBTITLES}_clone_${r}`;n&&(s[e.AUDIO]=n,e.AUDIO=n),o&&(i[e.SUBTITLES]=o,e.SUBTITLES=o);const l=fa(t.uri,e["STABLE-VARIANT-ID"],"PER-VARIANT-URIS",a),h=new es({attrs:e,audioCodec:t.audioCodec,bitrate:t.bitrate,height:t.height,name:t.name,url:l,videoCodec:t.videoCodec,width:t.width});if(t.audioGroups)for(let e=1;e{this.log(`Loaded steering manifest: "${i}"`);const n=t.data;if(1!==n.VERSION)return void this.log(`Steering VERSION ${n.VERSION} not supported!`);this.updated=performance.now(),this.timeToLoad=n.TTL;const{"RELOAD-URI":a,"PATHWAY-CLONES":o,"PATHWAY-PRIORITY":l}=n;if(a)try{this.uri=new self.URL(a,i).href}catch(t){return this.enabled=!1,void this.log(`Failed to parse Steering Manifest RELOAD-URI: ${a}`)}this.scheduleRefresh(this.uri||s.url),o&&this.clonePathways(o);const h={steeringManifest:n,url:i.toString()};this.hls.trigger(p.STEERING_MANIFEST_LOADED,h),l&&this.updatePathwayPriority(l)},onError:(t,e,s,i)=>{if(this.log(`Error loading steering manifest: ${t.code} ${t.text} (${e.url})`),this.stopLoad(),410===t.code)return this.enabled=!1,void this.log(`Steering manifest ${e.url} no longer available`);let r=1e3*this.timeToLoad;if(429!==t.code)this.scheduleRefresh(this.uri||e.url,r);else{const t=this.loader;if("function"==typeof(null==t?void 0:t.getResponseHeader)){const e=t.getResponseHeader("Retry-After");e&&(r=1e3*parseFloat(e))}this.log(`Steering manifest ${e.url} rate limited`)}},onTimeout:(t,e,s)=>{this.log(`Timeout loading steering manifest (${e.url})`),this.scheduleRefresh(this.uri||e.url)}};this.log(`Requesting steering manifest: ${i}`),this.loader.load(r,o,l)}scheduleRefresh(t,e=1e3*this.timeToLoad){this.clearTimeout(),this.reloadTimer=self.setTimeout((()=>{var e;const s=null==(e=this.hls)?void 0:e.media;!s||s.ended?this.scheduleRefresh(t,1e3*this.timeToLoad):this.loadSteeringManifest(t)}),e)}}});function Aa(t){return t&&"object"==typeof t?Array.isArray(t)?t.map(Aa):Object.keys(t).reduce(((e,s)=>(e[s]=Aa(t[s]),e)),{}):t}function Ra(t){const e=t.loader;if(e!==va&&e!==ma)A.log("[config]: Custom loader detected, cannot enable progressive streaming"),t.progressive=!1;else{(function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(t){}return!1})()&&(t.loader=va,t.progressive=!0,t.enableSoftwareAES=!0,A.log("[config]: Progressive streaming enabled, using FetchLoader"))}}let ba;class ka extends ws{constructor(t,e){super(t,"[level-controller]"),this._levels=[],this._firstLevel=-1,this._maxAutoLevel=-1,this._startLevel=void 0,this.currentLevel=null,this.currentLevelIndex=-1,this.manualLevelIndex=-1,this.steering=void 0,this.onParsedComplete=void 0,this.steering=e,this._registerListeners()}_registerListeners(){const{hls:t}=this;t.on(p.MANIFEST_LOADING,this.onManifestLoading,this),t.on(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(p.LEVEL_LOADED,this.onLevelLoaded,this),t.on(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(p.FRAG_BUFFERED,this.onFragBuffered,this),t.on(p.ERROR,this.onError,this)}_unregisterListeners(){const{hls:t}=this;t.off(p.MANIFEST_LOADING,this.onManifestLoading,this),t.off(p.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(p.LEVEL_LOADED,this.onLevelLoaded,this),t.off(p.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(p.FRAG_BUFFERED,this.onFragBuffered,this),t.off(p.ERROR,this.onError,this)}destroy(){this._unregisterListeners(),this.steering=null,this.resetLevels(),super.destroy()}stopLoad(){this._levels.forEach((t=>{t.loadError=0,t.fragmentError=0})),super.stopLoad()}resetLevels(){this._startLevel=void 0,this.manualLevelIndex=-1,this.currentLevelIndex=-1,this.currentLevel=null,this._levels=[],this._maxAutoLevel=-1}onManifestLoading(t,e){this.resetLevels()}onManifestLoaded(t,e){const s=this.hls.config.preferManagedMediaSource,i=[],r={},n={};let a=!1,o=!1,l=!1;e.levels.forEach((t=>{var e,h;const d=t.attrs;let{audioCodec:c,videoCodec:u}=t;-1!==(null==(e=c)?void 0:e.indexOf("mp4a.40.34"))&&(ba||(ba=/chrome|firefox/i.test(navigator.userAgent)),ba&&(t.audioCodec=c=void 0)),c&&(t.audioCodec=c=he(c,s)),0===(null==(h=u)?void 0:h.indexOf("avc1"))&&(u=t.videoCodec=function(t){const e=t.split(",");for(let t=0;t2){let i=s.shift()+".";i+=parseInt(s.shift()).toString(16),i+=("000"+parseInt(s.shift()).toString(16)).slice(-4),e[t]=i}}return e.join(",")}(u));const{width:f,height:g,unknownCodecs:m}=t;if(a||(a=!(!f||!g)),o||(o=!!u),l||(l=!!c),null!=m&&m.length||c&&!se(c,"audio",s)||u&&!se(u,"video",s))return;const{CODECS:p,"FRAME-RATE":v,"HDCP-LEVEL":y,"PATHWAY-ID":E,RESOLUTION:T,"VIDEO-RANGE":S}=d,L=`${`${E||"."}-`}${t.bitrate}-${T}-${v}-${p}-${S}-${y}`;if(r[L])if(r[L].uri===t.url||t.attrs["PATHWAY-ID"])r[L].addGroupId("audio",d.AUDIO),r[L].addGroupId("text",d.SUBTITLES);else{const e=n[L]+=1;t.attrs["PATHWAY-ID"]=new Array(e+1).join(".");const s=new es(t);r[L]=s,i.push(s)}else{const e=new es(t);r[L]=e,n[L]=1,i.push(e)}})),this.filterAndSortMediaOptions(i,e,a,o,l)}filterAndSortMediaOptions(t,e,s,i,r){let n=[],a=[],o=t;if((s||i)&&r&&(o=o.filter((({videoCodec:t,videoRange:e,width:s,height:i})=>{return(!!t||!(!s||!i))&&(!!(r=e)&&Xe.indexOf(r)>-1);var r}))),0===o.length)return void Promise.resolve().then((()=>{if(this.hls){e.levels.length&&this.warn(`One or more CODECS in variant not supported: ${JSON.stringify(e.levels[0].attrs)}`);const t=new Error("no level with compatible codecs found in manifest");this.hls.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.MANIFEST_INCOMPATIBLE_CODECS_ERROR,fatal:!0,url:e.url,error:t,reason:t.message})}}));if(e.audioTracks){const{preferManagedMediaSource:t}=this.hls.config;n=e.audioTracks.filter((e=>!e.audioCodec||se(e.audioCodec,"audio",t))),wa(n)}e.subtitles&&(a=e.subtitles,wa(a));const l=o.slice(0);o.sort(((t,e)=>{if(t.attrs["HDCP-LEVEL"]!==e.attrs["HDCP-LEVEL"])return(t.attrs["HDCP-LEVEL"]||"")>(e.attrs["HDCP-LEVEL"]||"")?1:-1;if(s&&t.height!==e.height)return t.height-e.height;if(t.frameRate!==e.frameRate)return t.frameRate-e.frameRate;if(t.videoRange!==e.videoRange)return Xe.indexOf(t.videoRange)-Xe.indexOf(e.videoRange);if(t.videoCodec!==e.videoCodec){const s=ne(t.videoCodec),i=ne(e.videoCodec);if(s!==i)return i-s}if(t.uri===e.uri&&t.codecSet!==e.codecSet){const s=ae(t.codecSet),i=ae(e.codecSet);if(s!==i)return i-s}return t.averageBitrate!==e.averageBitrate?t.averageBitrate-e.averageBitrate:0}));let h=l[0];if(this.steering&&(o=this.steering.filterParsedLevels(o),o.length!==l.length))for(let t=0;ts&&s===La.abrEwmaDefaultEstimate&&(this.hls.bandwidthEstimate=t)}break}const c=r&&!i,u={levels:o,audioTracks:n,subtitleTracks:a,sessionData:e.sessionData,sessionKeys:e.sessionKeys,firstLevel:this._firstLevel,stats:e.stats,audio:r,video:i,altAudio:!c&&n.some((t=>!!t.url))};this.hls.trigger(p.MANIFEST_PARSED,u),(this.hls.config.autoStartLoad||this.hls.forceStartLoad)&&this.hls.startLoad(this.hls.config.startPosition)}get levels(){return 0===this._levels.length?null:this._levels}get level(){return this.currentLevelIndex}set level(t){const e=this._levels;if(0===e.length)return;if(t<0||t>=e.length){const s=new Error("invalid level idx"),i=t<0;if(this.hls.trigger(p.ERROR,{type:v.OTHER_ERROR,details:y.LEVEL_SWITCH_ERROR,level:t,fatal:i,error:s,reason:s.message}),i)return;t=Math.min(t,e.length-1)}const s=this.currentLevelIndex,i=this.currentLevel,r=i?i.attrs["PATHWAY-ID"]:void 0,n=e[t],a=n.attrs["PATHWAY-ID"];if(this.currentLevelIndex=t,this.currentLevel=n,s===t&&n.details&&i&&r===a)return;this.log(`Switching to level ${t} (${n.height?n.height+"p ":""}${n.videoRange?n.videoRange+" ":""}${n.codecSet?n.codecSet+" ":""}@${n.bitrate})${a?" with Pathway "+a:""} from level ${s}${r?" with Pathway "+r:""}`);const o={level:t,attrs:n.attrs,details:n.details,bitrate:n.bitrate,averageBitrate:n.averageBitrate,maxBitrate:n.maxBitrate,realBitrate:n.realBitrate,width:n.width,height:n.height,codecSet:n.codecSet,audioCodec:n.audioCodec,videoCodec:n.videoCodec,audioGroups:n.audioGroups,subtitleGroups:n.subtitleGroups,loaded:n.loaded,loadError:n.loadError,fragmentError:n.fragmentError,name:n.name,id:n.id,uri:n.uri,url:n.url,urlId:0,audioGroupIds:n.audioGroupIds,textGroupIds:n.textGroupIds};this.hls.trigger(p.LEVEL_SWITCHING,o);const l=n.details;if(!l||l.live){const t=this.switchParams(n.uri,null==i?void 0:i.details,l);this.loadPlaylist(t)}}get manualLevel(){return this.manualLevelIndex}set manualLevel(t){this.manualLevelIndex=t,void 0===this._startLevel&&(this._startLevel=t),-1!==t&&(this.level=t)}get firstLevel(){return this._firstLevel}set firstLevel(t){this._firstLevel=t}get startLevel(){if(void 0===this._startLevel){const t=this.hls.config.startLevel;return void 0!==t?t:this.hls.firstAutoLevel}return this._startLevel}set startLevel(t){this._startLevel=t}onError(t,e){!e.fatal&&e.context&&e.context.type===be&&e.context.level===this.level&&this.checkRetry(e)}onFragBuffered(t,{frag:e}){if(void 0!==e&&e.type===De){const t=e.elementaryStreams;if(!Object.keys(t).some((e=>!!t[e])))return;const s=this._levels[e.level];null!=s&&s.loadError&&(this.log(`Resetting level error count of ${s.loadError} on frag buffered`),s.loadError=0)}}onLevelLoaded(t,e){var s;const{level:i,details:r}=e,n=this._levels[i];var a;if(!n)return this.warn(`Invalid level index ${i}`),void(null!=(a=e.deliveryDirectives)&&a.skip&&(r.deltaUpdateFailed=!0));i===this.currentLevelIndex?(0===n.fragmentError&&(n.loadError=0),this.playlistLoaded(i,e,n.details)):null!=(s=e.deliveryDirectives)&&s.skip&&(r.deltaUpdateFailed=!0)}loadPlaylist(t){super.loadPlaylist();const e=this.currentLevelIndex,s=this.currentLevel;if(s&&this.shouldLoadPlaylist(s)){let i=s.uri;if(t)try{i=t.addDirectives(i)}catch(t){this.warn(`Could not construct new URL with HLS Delivery Directives: ${t}`)}const r=s.attrs["PATHWAY-ID"];this.log(`Loading level index ${e}${void 0!==(null==t?void 0:t.msn)?" at sn "+t.msn+" part "+t.part:""} with${r?" Pathway "+r:""} ${i}`),this.clearTimer(),this.hls.trigger(p.LEVEL_LOADING,{url:i,level:e,pathwayId:s.attrs["PATHWAY-ID"],id:0,deliveryDirectives:t||null})}}get nextLoadLevel(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel}set nextLoadLevel(t){this.level=t,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=t)}removeLevel(t){var e;const s=this._levels.filter(((e,s)=>s!==t||(this.steering&&this.steering.removeLevel(e),e===this.currentLevel&&(this.currentLevel=null,this.currentLevelIndex=-1,e.details&&e.details.fragments.forEach((t=>t.level=-1))),!1)));ds(s),this._levels=s,this.currentLevelIndex>-1&&null!=(e=this.currentLevel)&&e.details&&(this.currentLevelIndex=this.currentLevel.details.fragments[0].level),this.hls.trigger(p.LEVELS_UPDATED,{levels:s})}onLevelsUpdated(t,{levels:e}){this._levels=e}checkMaxAutoUpdated(){const{autoLevelCapping:t,maxAutoLevel:e,maxHdcpLevel:s}=this.hls;this._maxAutoLevel!==e&&(this._maxAutoLevel=e,this.hls.trigger(p.MAX_AUTO_LEVEL_UPDATED,{autoLevelCapping:t,levels:this.levels,maxAutoLevel:e,minAutoLevel:this.hls.minAutoLevel,maxHdcpLevel:s}))}}function wa(t){const e={};t.forEach((t=>{const s=t.groupId||"";t.id=e[s]=e[s]||0,e[s]++}))}class Da{constructor(t){this.config=void 0,this.keyUriToKeyInfo={},this.emeController=null,this.config=t}abort(t){for(const s in this.keyUriToKeyInfo){const i=this.keyUriToKeyInfo[s].loader;if(i){var e;if(t&&t!==(null==(e=i.context)?void 0:e.frag.type))return;i.abort()}}}detach(){for(const t in this.keyUriToKeyInfo){const e=this.keyUriToKeyInfo[t];(e.mediaKeySessionContext||e.decryptdata.isCommonEncryption)&&delete this.keyUriToKeyInfo[t]}}destroy(){this.detach();for(const t in this.keyUriToKeyInfo){const e=this.keyUriToKeyInfo[t].loader;e&&e.destroy()}this.keyUriToKeyInfo={}}createKeyLoadError(t,e=y.KEY_LOAD_ERROR,s,i,r){return new ai({type:v.NETWORK_ERROR,details:e,fatal:!1,frag:t,response:r,error:s,networkDetails:i})}loadClear(t,e){if(this.emeController&&this.config.emeEnabled){const{sn:s,cc:i}=t;for(let t=0;t{r.setKeyFormat(t)}));break}}}}load(t){return!t.decryptdata&&t.encrypted&&this.emeController?this.emeController.selectKeySystemFormat(t).then((e=>this.loadInternal(t,e))):this.loadInternal(t)}loadInternal(t,e){var s,i;e&&t.setKeyFormat(e);const r=t.decryptdata;if(!r){const s=new Error(e?`Expected frag.decryptdata to be defined after setting format ${e}`:"Missing decryption data on fragment in onKeyLoading");return Promise.reject(this.createKeyLoadError(t,y.KEY_LOAD_ERROR,s))}const n=r.uri;if(!n)return Promise.reject(this.createKeyLoadError(t,y.KEY_LOAD_ERROR,new Error(`Invalid key URI: "${n}"`)));let a=this.keyUriToKeyInfo[n];if(null!=(s=a)&&s.decryptdata.key)return r.key=a.decryptdata.key,Promise.resolve({frag:t,keyInfo:a});var o;if(null!=(i=a)&&i.keyLoadPromise)switch(null==(o=a.mediaKeySessionContext)?void 0:o.keyStatus){case void 0:case"status-pending":case"usable":case"usable-in-future":return a.keyLoadPromise.then((e=>(r.key=e.keyInfo.decryptdata.key,{frag:t,keyInfo:a})))}switch(a=this.keyUriToKeyInfo[n]={decryptdata:r,keyLoadPromise:null,loader:null,mediaKeySessionContext:null},r.method){case"ISO-23001-7":case"SAMPLE-AES":case"SAMPLE-AES-CENC":case"SAMPLE-AES-CTR":return"identity"===r.keyFormat?this.loadKeyHTTP(a,t):this.loadKeyEME(a,t);case"AES-128":return this.loadKeyHTTP(a,t);default:return Promise.reject(this.createKeyLoadError(t,y.KEY_LOAD_ERROR,new Error(`Key supplied with unsupported METHOD: "${r.method}"`)))}}loadKeyEME(t,e){const s={frag:e,keyInfo:t};if(this.emeController&&this.config.emeEnabled){const e=this.emeController.loadKey(s);if(e)return(t.keyLoadPromise=e.then((e=>(t.mediaKeySessionContext=e,s)))).catch((e=>{throw t.keyLoadPromise=null,e}))}return Promise.resolve(s)}loadKeyHTTP(t,e){const s=this.config,i=new(0,s.loader)(s);return e.keyLoader=t.loader=i,t.keyLoadPromise=new Promise(((r,n)=>{const a={keyInfo:t,frag:e,responseType:"arraybuffer",url:t.decryptdata.uri},o=s.keyLoadPolicy.default,l={loadPolicy:o,timeout:o.maxLoadTimeMs,maxRetry:0,retryDelay:0,maxRetryDelay:0},d={onSuccess:(t,e,s,i)=>{const{frag:a,keyInfo:o,url:l}=s;if(!a.decryptdata||o!==this.keyUriToKeyInfo[l])return n(this.createKeyLoadError(a,y.KEY_LOAD_ERROR,new Error("after key load, decryptdata unset or changed"),i));o.decryptdata.key=a.decryptdata.key=new Uint8Array(t.data),a.keyLoader=null,o.loader=null,r({frag:a,keyInfo:o})},onError:(t,s,i,r)=>{this.resetLoader(s),n(this.createKeyLoadError(e,y.KEY_LOAD_ERROR,new Error(`HTTP Error ${t.code} loading key ${t.text}`),i,h({url:a.url,data:void 0},t)))},onTimeout:(t,s,i)=>{this.resetLoader(s),n(this.createKeyLoadError(e,y.KEY_LOAD_TIMEOUT,new Error("key loading timed out"),i))},onAbort:(t,s,i)=>{this.resetLoader(s),n(this.createKeyLoadError(e,y.INTERNAL_ABORTED,new Error("key loading aborted"),i))}};i.load(a,l,d)}))}resetLoader(t){const{frag:e,keyInfo:s,url:i}=t,r=s.loader;e.keyLoader===r&&(e.keyLoader=null,s.loader=null),delete this.keyUriToKeyInfo[i],r&&r.destroy()}}function Ia(){return self.SourceBuffer||self.WebKitSourceBuffer}function _a(){if(!te())return!1;const t=Ia();return!t||t.prototype&&"function"==typeof t.prototype.appendBuffer&&"function"==typeof t.prototype.remove}class Ca{constructor(t,e,s,i){this.config=void 0,this.media=null,this.fragmentTracker=void 0,this.hls=void 0,this.nudgeRetry=0,this.stallReported=!1,this.stalled=null,this.moved=!1,this.seeking=!1,this.config=t,this.media=e,this.fragmentTracker=s,this.hls=i}destroy(){this.media=null,this.hls=this.fragmentTracker=null}poll(t,e){const{config:s,media:i,stalled:r}=this;if(null===i)return;const{currentTime:n,seeking:a}=i,o=this.seeking&&!a,l=!this.seeking&&a;if(this.seeking=a,n!==t){if(this.moved=!0,a||(this.nudgeRetry=0),null!==r){if(this.stallReported){const t=self.performance.now()-r;A.warn(`playback not stuck anymore @${n}, after ${Math.round(t)}ms`),this.stallReported=!1}this.stalled=null}return}if(l||o)return void(this.stalled=null);if(i.paused&&!a||i.ended||0===i.playbackRate||!Xs.getBuffered(i).length)return void(this.nudgeRetry=0);const h=Xs.bufferInfo(i,n,0),d=h.nextStart||0;if(a){const t=h.len>2,s=!d||e&&e.start<=n||d-n>2&&!this.fragmentTracker.getPartialFragment(n);if(t||s)return;this.moved=!1}if(!this.moved&&null!==this.stalled){var c;if(!(h.len>0)&&!d)return;const t=Math.max(d,h.start||0)-n,e=this.hls.levels?this.hls.levels[this.hls.currentLevel]:null,s=(null==e||null==(c=e.details)?void 0:c.live)?2*e.details.targetduration:2,r=this.fragmentTracker.getPartialFragment(n);if(t>0&&(t<=s||r))return void(i.paused||this._trySkipBufferHole(r))}const u=self.performance.now();if(null===r)return void(this.stalled=u);const f=u-r;if(!a&&f>=250&&(this._reportStall(h),!this.media))return;const g=Xs.bufferInfo(i,n,s.maxBufferHole);this._tryFixBufferStall(g,f)}_tryFixBufferStall(t,e){const{config:s,fragmentTracker:i,media:r}=this;if(null===r)return;const n=r.currentTime,a=i.getPartialFragment(n);if(a){if(this._trySkipBufferHole(a)||!this.media)return}(t.len>s.maxBufferHole||t.nextStart&&t.nextStart-n1e3*s.highBufferWatchdogPeriod&&(A.warn("Trying to nudge playhead over buffer-hole"),this.stalled=null,this._tryNudgeBuffer())}_reportStall(t){const{hls:e,media:s,stallReported:i}=this;if(!i&&s){this.stallReported=!0;const i=new Error(`Playback stalling at @${s.currentTime} due to low buffer (${JSON.stringify(t)})`);A.warn(i.message),e.trigger(p.ERROR,{type:v.MEDIA_ERROR,details:y.BUFFER_STALLED_ERROR,fatal:!1,error:i,buffer:t.len})}}_trySkipBufferHole(t){const{config:e,hls:s,media:i}=this;if(null===i)return 0;const r=i.currentTime,n=Xs.bufferInfo(i,r,0),a=r0&&n.len<1&&i.readyState<3,h=a-r;if(h>0&&(o||l)){if(h>e.maxBufferHole){const{fragmentTracker:e}=this;let s=!1;if(0===r){const t=e.getAppendedFrag(0,De);t&&a1?(t=0,this.bitrateTest=!0):t=s.firstAutoLevel),s.nextLoadLevel=t,this.level=s.loadLevel,this.loadedmetadata=!1}e>0&&-1===t&&(this.log(`Override startPosition with lastCurrentTime @${e.toFixed(3)}`),t=e),this.state=fi,this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}else this._forceStartLoad=!0,this.state=ui}stopLoad(){this._forceStartLoad=!1,super.stopLoad()}doTick(){switch(this.state){case Ai:{const{levels:t,level:e}=this,s=null==t?void 0:t[e],i=null==s?void 0:s.details;if(i&&(!i.live||this.levelLastLoaded===s)){if(this.waitForCdnTuneIn(i))break;this.state=fi;break}if(this.hls.nextLoadLevel!==this.level){this.state=fi;break}break}case pi:{var t;const e=self.performance.now(),s=this.retryDate;if(!s||e>=s||null!=(t=this.media)&&t.seeking){const{levels:t,level:e}=this,s=null==t?void 0:t[e];this.resetStartWhenNotLoaded(s||null),this.state=fi}}}this.state===fi&&this.doTickIdle(),this.onTickEnd()}onTickEnd(){super.onTickEnd(),this.checkBuffer(),this.checkFragmentChanged()}doTickIdle(){const{hls:t,levelLastLoaded:e,levels:s,media:i}=this;if(null===e||!i&&(this.startFragRequested||!t.config.startFragPrefetch))return;if(this.altAudio&&this.audioOnly)return;const r=t.nextLoadLevel;if(null==s||!s[r])return;const n=s[r],a=this.getMainFwdBufferInfo();if(null===a)return;const o=this.getLevelDetails();if(o&&this._streamEnded(a,o)){const t={};return this.altAudio&&(t.type="video"),this.hls.trigger(p.BUFFER_EOS,t),void(this.state=Ti)}t.loadLevel!==r&&-1===t.manualLevel&&this.log(`Adapting to level ${r} from level ${this.level}`),this.level=t.nextLoadLevel=r;const l=n.details;if(!l||this.state===Ai||l.live&&this.levelLastLoaded!==n)return this.level=r,void(this.state=Ai);const h=a.len,d=this.getMaxBufferLength(n.maxBitrate);if(h>=d)return;this.backtrackFragment&&this.backtrackFragment.start>a.end&&(this.backtrackFragment=null);const c=this.backtrackFragment?this.backtrackFragment.start:a.end;let u=this.getNextFragment(c,l);if(this.couldBacktrack&&!this.fragPrevious&&u&&"initSegment"!==u.sn&&this.fragmentTracker.getState(u)!==Vs){var f;const t=(null!=(f=this.backtrackFragment)?f:u).sn-l.startSN,e=l.fragments[t-1];e&&u.cc===e.cc&&(u=e,this.fragmentTracker.removeFragment(e))}else this.backtrackFragment&&a.len&&(this.backtrackFragment=null);if(u&&this.isLoopLoading(u,c)){if(!u.gap){const t=this.audioOnly&&!this.altAudio?_:C,e=(t===C?this.videoBuffer:this.mediaBuffer)||this.media;e&&this.afterBufferFlushed(e,t,De)}u=this.getNextFragmentLoopLoading(u,l,a,De,d)}u&&(!u.initSegment||u.initSegment.data||this.bitrateTest||(u=u.initSegment),this.loadFragment(u,n,c))}loadFragment(t,e,s){const i=this.fragmentTracker.getState(t);this.fragCurrent=t,i===Gs||i===Hs?"initSegment"===t.sn?this._loadInitSegment(t,e):this.bitrateTest?(this.log(`Fragment ${t.sn} of level ${t.level} is being downloaded to test bitrate and will not be buffered`),this._loadBitrateTestFrag(t,e)):(this.startFragRequested=!0,super.loadFragment(t,e,s)):this.clearTrackerIfNeeded(t)}getBufferedFrag(t){return this.fragmentTracker.getBufferedFrag(t,De)}followingBufferedFrag(t){return t?this.getBufferedFrag(t.end+.5):null}immediateLevelSwitch(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)}nextLevelSwitch(){const{levels:t,media:e}=this;if(null!=e&&e.readyState){let s;const i=this.getAppendedFrag(e.currentTime);i&&i.start>1&&this.flushMainBuffer(0,i.start-1);const r=this.getLevelDetails();if(null!=r&&r.live){const t=this.getMainFwdBufferInfo();if(!t||t.len<2*r.targetduration)return}if(!e.paused&&t){const e=t[this.hls.nextLoadLevel],i=this.fragLastKbps;s=i&&this.fragCurrent?this.fragCurrent.duration*e.maxBitrate/(1e3*i)+1:0}else s=0;const n=this.getBufferedFrag(e.currentTime+s);if(n){const t=this.followingBufferedFrag(n);if(t){this.abortCurrentFrag();const e=t.maxStartPTS?t.maxStartPTS:t.start,s=t.duration,i=Math.max(n.end,e+Math.min(Math.max(s-this.config.maxFragLookUpTolerance,s*(this.couldBacktrack?.5:.125)),s*(this.couldBacktrack?.75:.25)));this.flushMainBuffer(i,Number.POSITIVE_INFINITY)}}}}abortCurrentFrag(){const t=this.fragCurrent;switch(this.fragCurrent=null,this.backtrackFragment=null,t&&(t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.state){case gi:case mi:case pi:case yi:case Ei:this.state=fi}this.nextLoadPosition=this.getLoadPosition()}flushMainBuffer(t,e){super.flushMainBuffer(t,e,this.altAudio?"video":null)}onMediaAttached(t,e){super.onMediaAttached(t,e);const s=e.media;this.onvplaying=this.onMediaPlaying.bind(this),this.onvseeked=this.onMediaSeeked.bind(this),s.addEventListener("playing",this.onvplaying),s.addEventListener("seeked",this.onvseeked),this.gapController=new Ca(this.config,s,this.fragmentTracker,this.hls)}onMediaDetaching(){const{media:t}=this;t&&this.onvplaying&&this.onvseeked&&(t.removeEventListener("playing",this.onvplaying),t.removeEventListener("seeked",this.onvseeked),this.onvplaying=this.onvseeked=null,this.videoBuffer=null),this.fragPlaying=null,this.gapController&&(this.gapController.destroy(),this.gapController=null),super.onMediaDetaching()}onMediaPlaying(){this.tick()}onMediaSeeked(){const t=this.media,e=t?t.currentTime:null;f(e)&&this.log(`Media seeked to ${e.toFixed(3)}`);const s=this.getMainFwdBufferInfo();null!==s&&0!==s.len?this.tick():this.warn(`Main forward buffer length on "seeked" event ${s?s.len:"empty"})`)}onManifestLoading(){this.log("Trigger BUFFER_RESET"),this.hls.trigger(p.BUFFER_RESET,void 0),this.fragmentTracker.removeAllFragments(),this.couldBacktrack=!1,this.startPosition=this.lastCurrentTime=this.fragLastKbps=0,this.levels=this.fragPlaying=this.backtrackFragment=this.levelLastLoaded=null,this.altAudio=this.audioOnly=this.startFragRequested=!1}onManifestParsed(t,e){let s=!1,i=!1;e.levels.forEach((t=>{const e=t.audioCodec;e&&(s=s||-1!==e.indexOf("mp4a.40.2"),i=i||-1!==e.indexOf("mp4a.40.5"))})),this.audioCodecSwitch=s&&i&&!function(){var t;const e=Ia();return"function"==typeof(null==e||null==(t=e.prototype)?void 0:t.changeType)}(),this.audioCodecSwitch&&this.log("Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC"),this.levels=e.levels,this.startFragRequested=!1}onLevelLoading(t,e){const{levels:s}=this;if(!s||this.state!==fi)return;const i=s[e.level];(!i.details||i.details.live&&this.levelLastLoaded!==i||this.waitForCdnTuneIn(i.details))&&(this.state=Ai)}onLevelLoaded(t,e){var s;const{levels:i}=this,r=e.level,n=e.details,a=n.totalduration;if(!i)return void this.warn(`Levels were reset while loading level ${r}`);this.log(`Level ${r} loaded [${n.startSN},${n.endSN}]${n.lastPartSn?`[part-${n.lastPartSn}-${n.lastPartIndex}]`:""}, cc [${n.startCC}, ${n.endCC}] duration:${a}`);const o=i[r],l=this.fragCurrent;!l||this.state!==mi&&this.state!==pi||l.level!==e.level&&l.loader&&this.abortCurrentFrag();let h=0;if(n.live||null!=(s=o.details)&&s.live){var d;if(this.checkLiveUpdate(n),n.deltaUpdateFailed)return;h=this.alignPlaylists(n,o.details,null==(d=this.levelLastLoaded)?void 0:d.details)}if(o.details=n,this.levelLastLoaded=o,this.hls.trigger(p.LEVEL_UPDATED,{details:n,level:r}),this.state===Ai){if(this.waitForCdnTuneIn(n))return;this.state=fi}this.startFragRequested?n.live&&this.synchronizeToLiveEdge(n):this.setStartPosition(n,h),this.tick()}_handleFragmentLoadProgress(t){var e;const{frag:s,part:i,payload:r}=t,{levels:n}=this;if(!n)return void this.warn(`Levels were reset while fragment load was in progress. Fragment ${s.sn} of level ${s.level} will not be buffered`);const a=n[s.level],o=a.details;if(!o)return this.warn(`Dropping fragment ${s.sn} of level ${s.level} after level details were reset`),void this.fragmentTracker.removeFragment(s);const l=a.videoCodec,h=o.PTSKnown||!o.live,d=null==(e=s.initSegment)?void 0:e.data,c=this._getAudioCodec(a),u=this.transmuxer=this.transmuxer||new Cr(this.hls,De,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)),f=i?i.index:-1,g=-1!==f,m=new zs(s.level,s.sn,s.stats.chunkCount,r.byteLength,f,g),p=this.initPTS[s.cc];u.push(r,d,c,l,s,i,o.totalduration,h,m,p)}onAudioTrackSwitching(t,e){const s=this.altAudio;if(!!!e.url){if(this.mediaBuffer!==this.media){this.log("Switching on main audio, use media.buffered to schedule main fragment loading"),this.mediaBuffer=this.media;const t=this.fragCurrent;t&&(this.log("Switching to main audio track, cancel main fragment load"),t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.resetTransmuxer(),this.resetLoadingState()}else this.audioOnly&&this.resetTransmuxer();const t=this.hls;s&&(t.trigger(p.BUFFER_FLUSHING,{startOffset:0,endOffset:Number.POSITIVE_INFINITY,type:null}),this.fragmentTracker.removeAllFragments()),t.trigger(p.AUDIO_TRACK_SWITCHED,e)}}onAudioTrackSwitched(t,e){const s=e.id,i=!!this.hls.audioTracks[s].url;if(i){const t=this.videoBuffer;t&&this.mediaBuffer!==t&&(this.log("Switching on alternate audio, use video.buffered to schedule main fragment loading"),this.mediaBuffer=t)}this.altAudio=i,this.tick()}onBufferCreated(t,e){const s=e.tracks;let i,r,n=!1;for(const t in s){const e=s[t];if("main"===e.id){if(r=t,i=e,"video"===t){const e=s[t];e&&(this.videoBuffer=e.buffer)}}else n=!0}n&&i?(this.log(`Alternate track found, use ${r}.buffered to schedule main fragment loading`),this.mediaBuffer=i.buffer):this.mediaBuffer=this.media}onFragBuffered(t,e){const{frag:s,part:i}=e;if(s&&s.type!==De)return;if(this.fragContextChanged(s))return this.warn(`Fragment ${s.sn}${i?" p: "+i.index:""} of level ${s.level} finished buffering, but was aborted. state: ${this.state}`),void(this.state===Ei&&(this.state=fi));const r=i?i.stats:s.stats;this.fragLastKbps=Math.round(8*r.total/(r.buffering.end-r.loading.first)),"initSegment"!==s.sn&&(this.fragPrevious=s),this.fragBufferedComplete(s,i)}onError(t,e){var s;if(e.fatal)this.state=Si;else switch(e.details){case y.FRAG_GAP:case y.FRAG_PARSING_ERROR:case y.FRAG_DECRYPT_ERROR:case y.FRAG_LOAD_ERROR:case y.FRAG_LOAD_TIMEOUT:case y.KEY_LOAD_ERROR:case y.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(De,e);break;case y.LEVEL_LOAD_ERROR:case y.LEVEL_LOAD_TIMEOUT:case y.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==Ai||(null==(s=e.context)?void 0:s.type)!==be||(this.state=fi);break;case y.BUFFER_APPEND_ERROR:case y.BUFFER_FULL_ERROR:if(!e.parent||"main"!==e.parent)return;if(e.details===y.BUFFER_APPEND_ERROR)return void this.resetLoadingState();this.reduceLengthAndFlushBuffer(e)&&this.flushMainBuffer(0,Number.POSITIVE_INFINITY);break;case y.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}}checkBuffer(){const{media:t,gapController:e}=this;if(t&&e&&t.readyState){if(this.loadedmetadata||!Xs.getBuffered(t).length){const t=this.state!==fi?this.fragCurrent:null;e.poll(this.lastCurrentTime,t)}this.lastCurrentTime=t.currentTime}}onFragLoadEmergencyAborted(){this.state=fi,this.loadedmetadata||(this.startFragRequested=!1,this.nextLoadPosition=this.startPosition),this.tickImmediate()}onBufferFlushed(t,{type:e}){if(e!==_||this.audioOnly&&!this.altAudio){const t=(e===C?this.videoBuffer:this.mediaBuffer)||this.media;this.afterBufferFlushed(t,e,De),this.tick()}}onLevelsUpdated(t,e){this.level>-1&&this.fragCurrent&&(this.level=this.fragCurrent.level),this.levels=e.levels}swapAudioCodec(){this.audioCodecSwap=!this.audioCodecSwap}seekToStartPos(){const{media:t}=this;if(!t)return;const e=t.currentTime;let s=this.startPosition;if(s>=0&&e0&&(r{const{hls:i}=this;if(!s||this.fragContextChanged(t))return;e.fragmentError=0,this.state=fi,this.startFragRequested=!1,this.bitrateTest=!1;const r=t.stats;r.parsing.start=r.parsing.end=r.buffering.start=r.buffering.end=self.performance.now(),i.trigger(p.FRAG_LOADED,s),t.bitrateTest=!1}))}_handleTransmuxComplete(t){var e;const s="main",{hls:i}=this,{remuxResult:r,chunkMeta:n}=t,a=this.getCurrentContext(n);if(!a)return void this.resetWhenMissingContext(n);const{frag:o,part:l,level:h}=a,{video:d,text:c,id3:u,initSegment:g}=r,{details:m}=h,v=this.altAudio?void 0:r.audio;if(this.fragContextChanged(o))this.fragmentTracker.removeFragment(o);else{if(this.state=yi,g){if(null!=g&&g.tracks){const t=o.initSegment||o;this._bufferInitSegment(h,g.tracks,t,n),i.trigger(p.FRAG_PARSING_INIT_SEGMENT,{frag:t,id:s,tracks:g.tracks})}const t=g.initPTS,e=g.timescale;f(t)&&(this.initPTS[o.cc]={baseTime:t,timescale:e},i.trigger(p.INIT_PTS_FOUND,{frag:o,id:s,initPTS:t,timescale:e}))}if(d&&m&&"initSegment"!==o.sn){const t=m.fragments[o.sn-1-m.startSN],e=o.sn===m.startSN,s=!t||o.cc>t.cc;if(!1!==r.independent){const{startPTS:t,endPTS:i,startDTS:r,endDTS:a}=d;if(l)l.elementaryStreams[d.type]={startPTS:t,endPTS:i,startDTS:r,endDTS:a};else if(d.firstKeyFrame&&d.independent&&1===n.id&&!s&&(this.couldBacktrack=!0),d.dropped&&d.independent){const r=this.getMainFwdBufferInfo(),n=(r?r.end:this.getLoadPosition())+this.config.maxBufferHole,l=d.firstKeyFramePTS?d.firstKeyFramePTS:t;if(!e&&n2&&(o.gap=!0);o.setElementaryStreamInfo(d.type,t,i,r,a),this.backtrackFragment&&(this.backtrackFragment=o),this.bufferFragmentData(d,o,l,n,e||s)}else{if(!e&&!s)return void this.backtrack(o);o.gap=!0}}if(v){const{startPTS:t,endPTS:e,startDTS:s,endDTS:i}=v;l&&(l.elementaryStreams[_]={startPTS:t,endPTS:e,startDTS:s,endDTS:i}),o.setElementaryStreamInfo(_,t,e,s,i),this.bufferFragmentData(v,o,l,n)}if(m&&null!=u&&null!=(e=u.samples)&&e.length){const t={id:s,frag:o,details:m,samples:u.samples};i.trigger(p.FRAG_PARSING_METADATA,t)}if(m&&c){const t={id:s,frag:o,details:m,samples:c.samples};i.trigger(p.FRAG_PARSING_USERDATA,t)}}}_bufferInitSegment(t,e,s,i){if(this.state!==yi)return;this.audioOnly=!!e.audio&&!e.video,this.altAudio&&!this.audioOnly&&delete e.audio;const{audio:r,video:n,audiovideo:a}=e;if(r){let e=t.audioCodec;const s=navigator.userAgent.toLowerCase();if(this.audioCodecSwitch){e&&(e=-1!==e.indexOf("mp4a.40.5")?"mp4a.40.2":"mp4a.40.5");const t=r.metadata;t&&"channelCount"in t&&1!==(t.channelCount||1)&&-1===s.indexOf("firefox")&&(e="mp4a.40.5")}e&&-1!==e.indexOf("mp4a.40.5")&&-1!==s.indexOf("android")&&"audio/mpeg"!==r.container&&(e="mp4a.40.2",this.log(`Android: force audio codec to ${e}`)),t.audioCodec&&t.audioCodec!==e&&this.log(`Swapping manifest audio codec "${t.audioCodec}" for "${e}"`),r.levelCodec=e,r.id="main",this.log(`Init audio buffer, container:${r.container}, codecs[selected/level/parsed]=[${e||""}/${t.audioCodec||""}/${r.codec}]`)}n&&(n.levelCodec=t.videoCodec,n.id="main",this.log(`Init video buffer, container:${n.container}, codecs[level/parsed]=[${t.videoCodec||""}/${n.codec}]`)),a&&this.log(`Init audiovideo buffer, container:${a.container}, codecs[level/parsed]=[${t.codecs}/${a.codec}]`),this.hls.trigger(p.BUFFER_CODECS,e),Object.keys(e).forEach((t=>{const r=e[t].initSegment;null!=r&&r.byteLength&&this.hls.trigger(p.BUFFER_APPENDING,{type:t,data:r,frag:s,part:null,chunkMeta:i,parent:s.type})})),this.tickImmediate()}getMainFwdBufferInfo(){return this.getFwdBufferInfo(this.mediaBuffer?this.mediaBuffer:this.media,De)}backtrack(t){this.couldBacktrack=!0,this.backtrackFragment=t,this.resetTransmuxer(),this.flushBufferGap(t),this.fragmentTracker.removeFragment(t),this.fragPrevious=null,this.nextLoadPosition=t.start,this.state=fi}checkFragmentChanged(){const t=this.media;let e=null;if(t&&t.readyState>1&&!1===t.seeking){const s=t.currentTime;if(Xs.isBuffered(t,s)?e=this.getAppendedFrag(s):Xs.isBuffered(t,s+.1)&&(e=this.getAppendedFrag(s+.1)),e){this.backtrackFragment=null;const t=this.fragPlaying,s=e.level;t&&e.sn===t.sn&&t.level===s||(this.fragPlaying=e,this.hls.trigger(p.FRAG_CHANGED,{frag:e}),t&&t.level===s||this.hls.trigger(p.LEVEL_SWITCHED,{level:s}))}}}get nextLevel(){const t=this.nextBufferedFrag;return t?t.level:-1}get currentFrag(){const t=this.media;return t?this.fragPlaying||this.getAppendedFrag(t.currentTime):null}get currentProgramDateTime(){const t=this.media;if(t){const e=t.currentTime,s=this.currentFrag;if(s&&f(e)&&f(s.programDateTime)){const t=s.programDateTime+1e3*(e-s.start);return new Date(t)}}return null}get currentLevel(){const t=this.currentFrag;return t?t.level:-1}get nextBufferedFrag(){const t=this.currentFrag;return t?this.followingBufferedFrag(t):null}get forceStartLoad(){return this._forceStartLoad}}class Pa{static get version(){return"1.5.15"}static isMSESupported(){return _a()}static isSupported(){return function(){if(!_a())return!1;const t=te();return"function"==typeof(null==t?void 0:t.isTypeSupported)&&(["avc1.42E01E,mp4a.40.2","av01.0.01M.08","vp09.00.50.08"].some((e=>t.isTypeSupported(re(e,"video"))))||["mp4a.40.2","fLaC"].some((e=>t.isTypeSupported(re(e,"audio")))))}()}static getMediaSource(){return te()}static get Events(){return p}static get ErrorTypes(){return v}static get ErrorDetails(){return y}static get DefaultConfig(){return Pa.defaultConfig?Pa.defaultConfig:La}static set DefaultConfig(t){Pa.defaultConfig=t}constructor(t={}){this.config=void 0,this.userConfig=void 0,this.coreComponents=void 0,this.networkControllers=void 0,this.started=!1,this._emitter=new _r,this._autoLevelCapping=-1,this._maxHdcpLevel=null,this.abrController=void 0,this.bufferController=void 0,this.capLevelController=void 0,this.latencyController=void 0,this.levelController=void 0,this.streamController=void 0,this.audioTrackController=void 0,this.subtitleTrackController=void 0,this.emeController=void 0,this.cmcdController=void 0,this._media=null,this.url=null,this.triggeringException=void 0,function(t,e){if("object"==typeof console&&!0===t||"object"==typeof t){L(t,"debug","log","info","warn","error");try{S.log(`Debug logs enabled for "${e}" in hls.js version 1.5.15`)}catch(t){S=T}}else S=T}(t.debug||!1,"Hls instance");const e=this.config=function(t,e){if((e.liveSyncDurationCount||e.liveMaxLatencyDurationCount)&&(e.liveSyncDuration||e.liveMaxLatencyDuration))throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");if(void 0!==e.liveMaxLatencyDurationCount&&(void 0===e.liveSyncDurationCount||e.liveMaxLatencyDurationCount<=e.liveSyncDurationCount))throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');if(void 0!==e.liveMaxLatencyDuration&&(void 0===e.liveSyncDuration||e.liveMaxLatencyDuration<=e.liveSyncDuration))throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"');const s=Aa(t),i=["TimeOut","MaxRetry","RetryDelay","MaxRetryTimeout"];return["manifest","level","frag"].forEach((t=>{const r=`${"level"===t?"playlist":t}LoadPolicy`,n=void 0===e[r],a=[];i.forEach((i=>{const o=`${t}Loading${i}`,l=e[o];if(void 0!==l&&n){a.push(o);const t=s[r].default;switch(e[r]={default:t},i){case"TimeOut":t.maxLoadTimeMs=l,t.maxTimeToFirstByteMs=l;break;case"MaxRetry":t.errorRetry.maxNumRetry=l,t.timeoutRetry.maxNumRetry=l;break;case"RetryDelay":t.errorRetry.retryDelayMs=l,t.timeoutRetry.retryDelayMs=l;break;case"MaxRetryTimeout":t.errorRetry.maxRetryDelayMs=l,t.timeoutRetry.maxRetryDelayMs=l}}})),a.length&&A.warn(`hls.js config: "${a.join('", "')}" setting(s) are deprecated, use "${r}": ${JSON.stringify(e[r])}`)})),h(h({},s),e)}(Pa.DefaultConfig,t);this.userConfig=t,e.progressive&&Ra(e);const{abrController:s,bufferController:i,capLevelController:r,errorController:n,fpsController:a}=e,o=new n(this),l=this.abrController=new s(this),d=this.bufferController=new i(this),c=this.capLevelController=new r(this),u=new a(this),f=new Pe(this),g=new We(this),m=e.contentSteeringController,v=m?new m(this):null,y=this.levelController=new ka(this,v),E=new Ys(this),R=new Da(this.config),b=this.streamController=new xa(this,E,R);c.setStreamController(b),u.setStreamController(b);const k=[f,y,b];v&&k.splice(1,0,v),this.networkControllers=k;const w=[l,d,c,u,g,E];this.audioTrackController=this.createController(e.audioTrackController,k);const D=e.audioStreamController;D&&k.push(new D(this,E,R)),this.subtitleTrackController=this.createController(e.subtitleTrackController,k);const I=e.subtitleStreamController;I&&k.push(new I(this,E,R)),this.createController(e.timelineController,w),R.emeController=this.emeController=this.createController(e.emeController,w),this.cmcdController=this.createController(e.cmcdController,w),this.latencyController=this.createController(je,w),this.coreComponents=w,k.push(o);const _=o.onErrorOut;"function"==typeof _&&this.on(p.ERROR,_,o)}createController(t,e){if(t){const s=new t(this);return e&&e.push(s),s}return null}on(t,e,s=this){this._emitter.on(t,e,s)}once(t,e,s=this){this._emitter.once(t,e,s)}removeAllListeners(t){this._emitter.removeAllListeners(t)}off(t,e,s=this,i){this._emitter.off(t,e,s,i)}listeners(t){return this._emitter.listeners(t)}emit(t,e,s){return this._emitter.emit(t,e,s)}trigger(t,e){if(this.config.debug)return this.emit(t,t,e);try{return this.emit(t,t,e)}catch(e){if(A.error("An internal error happened while handling event "+t+'. Error message: "'+e.message+'". Here is a stacktrace:',e),!this.triggeringException){this.triggeringException=!0;const s=t===p.ERROR;this.trigger(p.ERROR,{type:v.OTHER_ERROR,details:y.INTERNAL_EXCEPTION,fatal:s,event:t,error:e}),this.triggeringException=!1}}return!1}listenerCount(t){return this._emitter.listenerCount(t)}destroy(){A.log("destroy"),this.trigger(p.DESTROYING,void 0),this.detachMedia(),this.removeAllListeners(),this._autoLevelCapping=-1,this.url=null,this.networkControllers.forEach((t=>t.destroy())),this.networkControllers.length=0,this.coreComponents.forEach((t=>t.destroy())),this.coreComponents.length=0;const t=this.config;t.xhrSetup=t.fetchSetup=void 0,this.userConfig=null}attachMedia(t){A.log("attachMedia"),this._media=t,this.trigger(p.MEDIA_ATTACHING,{media:t})}detachMedia(){A.log("detachMedia"),this.trigger(p.MEDIA_DETACHING,void 0),this._media=null}loadSource(t){this.stopLoad();const e=this.media,s=this.url,i=this.url=o.buildAbsoluteURL(self.location.href,t,{alwaysNormalize:!0});this._autoLevelCapping=-1,this._maxHdcpLevel=null,A.log(`loadSource:${i}`),e&&s&&(s!==i||this.bufferController.hasSourceTypes())&&(this.detachMedia(),this.attachMedia(e)),this.trigger(p.MANIFEST_LOADING,{url:t})}startLoad(t=-1){A.log(`startLoad(${t})`),this.started=!0,this.networkControllers.forEach((e=>{e.startLoad(t)}))}stopLoad(){A.log("stopLoad"),this.started=!1,this.networkControllers.forEach((t=>{t.stopLoad()}))}resumeBuffering(){this.started&&this.networkControllers.forEach((t=>{"fragmentLoader"in t&&t.startLoad(-1)}))}pauseBuffering(){this.networkControllers.forEach((t=>{"fragmentLoader"in t&&t.stopLoad()}))}swapAudioCodec(){A.log("swapAudioCodec"),this.streamController.swapAudioCodec()}recoverMediaError(){A.log("recoverMediaError");const t=this._media;this.detachMedia(),t&&this.attachMedia(t)}removeLevel(t){this.levelController.removeLevel(t)}get levels(){const t=this.levelController.levels;return t||[]}get currentLevel(){return this.streamController.currentLevel}set currentLevel(t){A.log(`set currentLevel:${t}`),this.levelController.manualLevel=t,this.streamController.immediateLevelSwitch()}get nextLevel(){return this.streamController.nextLevel}set nextLevel(t){A.log(`set nextLevel:${t}`),this.levelController.manualLevel=t,this.streamController.nextLevelSwitch()}get loadLevel(){return this.levelController.level}set loadLevel(t){A.log(`set loadLevel:${t}`),this.levelController.manualLevel=t}get nextLoadLevel(){return this.levelController.nextLoadLevel}set nextLoadLevel(t){this.levelController.nextLoadLevel=t}get firstLevel(){return Math.max(this.levelController.firstLevel,this.minAutoLevel)}set firstLevel(t){A.log(`set firstLevel:${t}`),this.levelController.firstLevel=t}get startLevel(){const t=this.levelController.startLevel;return-1===t&&this.abrController.forcedAutoLevel>-1?this.abrController.forcedAutoLevel:t}set startLevel(t){A.log(`set startLevel:${t}`),-1!==t&&(t=Math.max(t,this.minAutoLevel)),this.levelController.startLevel=t}get capLevelToPlayerSize(){return this.config.capLevelToPlayerSize}set capLevelToPlayerSize(t){const e=!!t;e!==this.config.capLevelToPlayerSize&&(e?this.capLevelController.startCapping():(this.capLevelController.stopCapping(),this.autoLevelCapping=-1,this.streamController.nextLevelSwitch()),this.config.capLevelToPlayerSize=e)}get autoLevelCapping(){return this._autoLevelCapping}get bandwidthEstimate(){const{bwEstimator:t}=this.abrController;return t?t.getEstimate():NaN}set bandwidthEstimate(t){this.abrController.resetEstimator(t)}get ttfbEstimate(){const{bwEstimator:t}=this.abrController;return t?t.getEstimateTTFB():NaN}set autoLevelCapping(t){this._autoLevelCapping!==t&&(A.log(`set autoLevelCapping:${t}`),this._autoLevelCapping=t,this.levelController.checkMaxAutoUpdated())}get maxHdcpLevel(){return this._maxHdcpLevel}set maxHdcpLevel(t){(function(t){return qe.indexOf(t)>-1})(t)&&this._maxHdcpLevel!==t&&(this._maxHdcpLevel=t,this.levelController.checkMaxAutoUpdated())}get autoLevelEnabled(){return-1===this.levelController.manualLevel}get manualLevel(){return this.levelController.manualLevel}get minAutoLevel(){const{levels:t,config:{minAutoBitrate:e}}=this;if(!t)return 0;const s=t.length;for(let i=0;i=e)return i;return 0}get maxAutoLevel(){const{levels:t,autoLevelCapping:e,maxHdcpLevel:s}=this;let i;if(i=-1===e&&null!=t&&t.length?t.length-1:e,s)for(let e=i;e--;){const i=t[e].attrs["HDCP-LEVEL"];if(i&&i<=s)return e}return i}get firstAutoLevel(){return this.abrController.firstAutoLevel}get nextAutoLevel(){return this.abrController.nextAutoLevel}set nextAutoLevel(t){this.abrController.nextAutoLevel=t}get playingDate(){return this.streamController.currentProgramDateTime}get mainForwardBufferInfo(){return this.streamController.getMainFwdBufferInfo()}setAudioOption(t){var e;return null==(e=this.audioTrackController)?void 0:e.setAudioOption(t)}setSubtitleOption(t){var e;return null==(e=this.subtitleTrackController)||e.setSubtitleOption(t),null}get allAudioTracks(){const t=this.audioTrackController;return t?t.allAudioTracks:[]}get audioTracks(){const t=this.audioTrackController;return t?t.audioTracks:[]}get audioTrack(){const t=this.audioTrackController;return t?t.audioTrack:-1}set audioTrack(t){const e=this.audioTrackController;e&&(e.audioTrack=t)}get allSubtitleTracks(){const t=this.subtitleTrackController;return t?t.allSubtitleTracks:[]}get subtitleTracks(){const t=this.subtitleTrackController;return t?t.subtitleTracks:[]}get subtitleTrack(){const t=this.subtitleTrackController;return t?t.subtitleTrack:-1}get media(){return this._media}set subtitleTrack(t){const e=this.subtitleTrackController;e&&(e.subtitleTrack=t)}get subtitleDisplay(){const t=this.subtitleTrackController;return!!t&&t.subtitleDisplay}set subtitleDisplay(t){const e=this.subtitleTrackController;e&&(e.subtitleDisplay=t)}get lowLatencyMode(){return this.config.lowLatencyMode}set lowLatencyMode(t){this.config.lowLatencyMode=t}get liveSyncPosition(){return this.latencyController.liveSyncPosition}get latency(){return this.latencyController.latency}get maxLatency(){return this.latencyController.maxLatency}get targetLatency(){return this.latencyController.targetLatency}get drift(){return this.latencyController.drift}get forceStartLoad(){return this.streamController.forceStartLoad}}Pa.defaultConfig=void 0;class Ma{constructor(){this._ranges=[]}get length(){return this._ranges.length}start(t){if(t<0||t>=this._ranges.length)throw new DOMException("Invalid index","IndexSizeError");return this._ranges[t].start}end(t){if(t<0||t>=this._ranges.length)throw new DOMException("Invalid index","IndexSizeError");return this._ranges[t].end}_addRange(t,e){this._ranges.push({start:t,end:e}),this._normalizeRanges()}_removeRange(t,e){let s=[];for(let i of this._ranges)i.end<=t||i.start>=e?s.push(i):i.starte?(s.push({start:i.start,end:t}),s.push({start:e,end:i.end})):i.start>=t&&i.end<=e||(i.startt&&i.end<=e?s.push({start:i.start,end:t}):i.start>=t&&i.starte&&s.push({start:e,end:i.end}));this._ranges=s}_normalizeRanges(){this._ranges.sort(((t,e)=>t.start-e.start));let t=[];for(let e of this._ranges)if(0===t.length)t.push(e);else{let s=t[t.length-1];e.start<=s.end?s.end=Math.max(s.end,e.end):t.push(e)}this._ranges=t}}class Fa extends EventTarget{constructor(t="",e="",s=""){super(),this.kind=t,this.label=e,this.language=s,this.mode="disabled",this.cues=new Oa,this.activeCues=new Oa}addCue(t){this.cues._add(t)}removeCue(t){this.cues._remove(t)}}class Oa{constructor(){this._cues=[]}get length(){return this._cues.length}item(t){return this._cues[t]}getCueById(t){return this._cues.find((e=>e.id===t))||null}_add(t){this._cues.push(t)}_remove(t){const e=this._cues.indexOf(t);-1!==e&&this._cues.splice(e,1)}[Symbol.iterator](){return this._cues[Symbol.iterator]()}}class Na extends EventTarget{constructor(){super(),this._tracks=[]}get length(){return this._tracks.length}item(t){return this._tracks[t]}_add(t){this._tracks.push(t),this.dispatchEvent(new Event("addtrack"))}_remove(t){const e=this._tracks.indexOf(t);-1!==e&&(this._tracks.splice(e,1),this.dispatchEvent(new Event("removetrack")))}[Symbol.iterator](){return this._tracks[Symbol.iterator]()}}class Ua extends EventTarget{constructor(t){super(),this.instanceId=t,this.bridgeId=window.nextInternalId,window.nextInternalId+=1,window.bridgeObjectMap[this.bridgeId]=this,this._currentTime=0,this.duration=NaN,this.paused=!0,this._playbackRate=1,this.volume=1,this.muted=!1,this.readyState=0,this.networkState=0,this.buffered=new Ma,this.seeking=!1,this.loop=!1,this.autoplay=!1,this.controls=!1,this.error=null,this._src="",this.videoWidth=0,this.videoHeight=0,this.textTracks=new Na,this.isWaiting=!1,this.currentMedia=null,window.bridgeInvokeAsync(this.bridgeId,"VideoElement","constructor",{instanceId:this.instanceId}),setTimeout((()=>{this.readyState=4,this.dispatchEvent(new Event("loadedmetadata")),this.dispatchEvent(new Event("loadeddata")),this.dispatchEvent(new Event("canplay")),this.dispatchEvent(new Event("canplaythrough"))}),0)}get currentTime(){return this._currentTime}set currentTime(t){this._currentTime!=t&&(this._currentTime=t,this.dispatchEvent(new Event("seeking")),window.bridgeInvokeAsync(this.bridgeId,"VideoElement","setCurrentTime",{instanceId:this.instanceId,currentTime:t}).then((t=>{this.dispatchEvent(new Event("seeked"))})))}get playbackRate(){return this._playbackRate}set playbackRate(t){this._playbackRate=t,window.bridgeInvokeAsync(this.bridgeId,"VideoElement","setPlaybackRate",{instanceId:this.instanceId,playbackRate:t}).then((t=>{}))}get src(){return this._src}set src(t){this.currentMedia&&this.currentMedia.removeEventListener("bufferChanged",!1),this._src=t;var e=window.mediaSourceMap[this._src];this.currentMedia=e,e&&(e.addEventListener("bufferChanged",(()=>{this.updateBufferedFromMediaSource()}),!1),window.bridgeInvokeAsync(this.bridgeId,"VideoElement","setMediaSource",{instanceId:this.instanceId,mediaSourceId:e.bridgeId}).then((t=>{})))}removeAttribute(t){"src"===t&&(this._src="")}querySelectorAll(t){return document.createDocumentFragment().querySelectorAll("*")}updateBufferedFromMediaSource(){var t=this.currentMedia;this.buffered._ranges=t?t.getBufferedRanges():[]}bridgeUpdateStatus(t){var e=!t.isPlaying,s=t.isWaiting,i=t.currentTime;this.paused!=e&&(this.paused=e,e?this.dispatchEvent(new Event("pause")):(this.dispatchEvent(new Event("play")),this.dispatchEvent(new Event("playing")))),this.isWaiting!=s&&(this.isWaiting=s,s&&this.dispatchEvent(new Event("waiting"))),this._currentTime!=i&&(this._currentTime=i,this.dispatchEvent(new Event("timeupdate")))}play(){return this.paused?window.bridgeInvokeAsync(this.bridgeId,"VideoElement","play",{instanceId:this.instanceId}).then((t=>{this.dispatchEvent(new Event("play")),this.dispatchEvent(new Event("playing"))})):Promise.resolve()}pause(){if(!this.paused)return this.paused=!0,this.dispatchEvent(new Event("pause")),window.bridgeInvokeAsync(this.bridgeId,"VideoElement","pause",{instanceId:this.instanceId}).then((t=>{}))}canPlayType(t){return"probably"}addTextTrack(t,e,s){const i=new Fa(t,e,s);return this.textTracks._add(i),i}load(){}notifySeeked(){this.dispatchEvent(new Event("seeking")),this.dispatchEvent(new Event("seeked"))}}function Ba(t){const e=Array.from(t,(t=>String.fromCodePoint(t))).join("");return btoa(e)}class $a extends EventTarget{constructor(){super(),this._buffers=[]}_add(t){this._buffers.push(t),this.dispatchEvent(new Event("addsourcebuffer"))}_remove(t){const e=this._buffers.indexOf(t);return-1!==e&&(this._buffers.splice(e,1),this.dispatchEvent(new Event("removesourcebuffer")),!0)}get length(){return this._buffers.length}item(t){return this._buffers[t]}[Symbol.iterator](){return this._buffers[Symbol.iterator]()}}class Ga extends EventTarget{constructor(t,e){super(),this.mediaSource=t,this.mimeType=e,this.updating=!1,this.buffered=new Ma,this.timestampOffset=0,this.appendWindowStart=0,this.appendWindowEnd=1/0,this.bridgeId=window.nextInternalId,window.nextInternalId+=1,window.bridgeObjectMap[this.bridgeId]=this,window.bridgeInvokeAsync(this.bridgeId,"SourceBuffer","constructor",{mediaSourceId:this.mediaSource.bridgeId,mimeType:e})}appendBuffer(t){if(this.updating)throw new DOMException("SourceBuffer is updating","InvalidStateError");this.updating=!0,this.dispatchEvent(new Event("updatestart")),window.bridgeInvokeAsync(this.bridgeId,"SourceBuffer","appendBuffer",{data:Ba(t)}).then((t=>{const e=t.ranges;for(var s=[],i=0;i{})))}remove(t,e){if(this.updating)throw new DOMException("SourceBuffer is updating","InvalidStateError");this.updating=!0,this.dispatchEvent(new Event("updatestart")),window.bridgeInvokeAsync(this.bridgeId,"SourceBuffer","remove",{start:t,end:e}).then((t=>{const e=t.ranges;for(var s=[],i=0;i{this.readyState="open",this.dispatchEvent(new Event("sourceopen"))}),0)}static isTypeSupported(t){return!0}emitUpdatedBuffer(){this.dispatchEvent(new Event("bufferChanged"))}getBufferedRanges(){return 0!=this.sourceBuffers._buffers.length?this.sourceBuffers._buffers[0].buffered._ranges:[]}addSourceBuffer(t){if("open"!==this.readyState)throw new DOMException("MediaSource is not open","InvalidStateError");const e=new Ga(this,t);return this.sourceBuffers._add(e),this.activeSourceBuffers._add(e),this.dispatchEvent(new Event("bufferChanged")),window.bridgeInvokeAsync(this.bridgeId,"MediaSource","updateSourceBuffers",{ids:this.sourceBuffers._buffers.map((t=>t.bridgeId))}).then((t=>{})),e}removeSourceBuffer(t){if(!this.sourceBuffers._remove(t))throw new DOMException("SourceBuffer not found","NotFoundError");this.activeSourceBuffers._remove(t),this.dispatchEvent(new Event("bufferChanged")),window.bridgeInvokeAsync(this.bridgeId,"MediaSource","updateSourceBuffers",{ids:this.sourceBuffers._buffers.map((t=>t.bridgeId))}).then((t=>{}))}endOfStream(t){if("open"!==this.readyState)throw new DOMException("MediaSource is not open","InvalidStateError");this.readyState="ended",this.dispatchEvent(new Event("sourceended"))}_reopen(){"open"!==this.readyState&&(this.readyState="open",this.dispatchEvent(new Event("sourceopen")))}set duration(t){if("closed"===this.readyState)throw new DOMException("MediaSource is closed","InvalidStateError");this._duration=t,window.bridgeInvokeAsync(this.bridgeId,"MediaSource","setDuration",{duration:t}).then((t=>{}))}get duration(){return this._duration}}class Ha extends EventTarget{constructor(){super(),this.bridgeId=window.nextInternalId,window.nextInternalId+=1,this.readyState=0,this.status=0,this.statusText="",this.responseText="",this.responseXML=null,this._responseData=null,this.onreadystatechange=null,this._requestHeaders={},this._responseHeaders={},this._method="",this._url="",this._async=!0,this._user=null,this._password=null,this._responseType=""}open(t,e,s=!0,i=null,r=null){this._method=t,this._url=e,this._async=s,this._user=i,this._password=r,this.readyState=1,this._triggerReadyStateChange()}setRequestHeader(t,e){this._requestHeaders[t]=e}getResponseHeader(t){return this._responseHeaders[t.toLowerCase()]||null}getAllResponseHeaders(){return Object.entries(this._responseHeaders).map((([t,e])=>`${t}: ${e}`)).join("\r\n")}send(t=null){this.readyState=2,this._triggerReadyStateChange(),this.readyState=3,this._triggerReadyStateChange(),this.dispatchEvent(new Event("loadstart")),window.bridgeInvokeAsync(this.bridgeId,"XMLHttpRequest","load",{id:this.bridgeId,url:this._url,requestHeaders:this._requestHeaders}).then((t=>{t.error?this.dispatchEvent(new Event("error")):(this.status=t.status,this.statusText=t.statusText,t.responseData?("arraybuffer"===this._responseType?this._responseData=function(t){for(var e=atob(t),s=new Uint8Array(e.length),i=0;i{this.refreshPlayerStatus()})),this.video.addEventListener("pause",(()=>{this.refreshPlayerStatus()})),this.video.addEventListener("seeking",(()=>{this.refreshPlayerStatus()})),this.video.addEventListener("waiting",(()=>{this.refreshPlayerStatus()})),this.hls=new Pa({startLevel:0,testBandwidth:!1,debug:t.debug||!0,autoStartLoad:!1,backBufferLength:30,maxBufferLength:60,maxMaxBufferLength:60,maxFragLookUpTolerance:.001,nudgeMaxRetry:1e4}),this.hls.on(Pa.Events.MANIFEST_PARSED,(()=>{this.isManifestParsed=!0,this.refreshPlayerStatus()})),this.hls.on(Pa.Events.LEVEL_SWITCHED,(()=>{this.refreshPlayerStatus()})),this.hls.on(Pa.Events.LEVELS_UPDATED,(()=>{this.refreshPlayerStatus()})),this.hls.loadSource(t.urlPrefix+"master.m3u8"),this.hls.attachMedia(this.video)}playerLoad(t){this.hls.startLevel=t,this.hls.startLoad(-1,!1)}playerPlay(){this.video.play()}playerPause(){this.video.pause()}playerSetBaseRate(t){this.video.playbackRate=t}playerSetLevel(t){this.hls.currentLevel=t>=0?t:-1}playerSeek(t){this.video.currentTime=t}playerSetIsMuted(t){this.video.muted=t}getLevels(){for(var t=[],e=0;e2&&(t=!0),Va(this.id,"playerStatus",{isReady:this.isManifestParsed,isPlaying:!this.video.paused,rate:t?this.video.playbackRate:0,defaultRate:this.video.playbackRate,levels:this.getLevels(),currentLevel:this.hls.currentLevel}),this.refreshPlayerCurrentTime(),t?null==this.currentTimeUpdateTimeout&&(this.currentTimeUpdateTimeout=setTimeout((()=>{this.refreshPlayerCurrentTime()}),200)):null!=this.currentTimeUpdateTimeout&&(clearTimeout(this.currentTimeUpdateTimeout),this.currentTimeUpdateTimeout=null),this.notifySeekedOnNextStatusUpdate&&(this.notifySeekedOnNextStatusUpdate=!1,this.video.notifySeeked())}playerNotifySeekedOnNextStatusUpdate(){this.notifySeekedOnNextStatusUpdate=!0}refreshPlayerCurrentTime(){Va(this.id,"playerCurrentTime",{value:this.video.currentTime}),this.currentTimeUpdateTimeout=setTimeout((()=>{this.refreshPlayerCurrentTime()}),200)}}window.invokeOnLoad=function(){Va(this.id,"windowOnLoad",{})},window.onload=()=>{window.invokeOnLoad()},window.hlsPlayer_instances={},window.hlsPlayer_makeInstance=function(t){window.hlsPlayer_instances[t]=new Ya(t)},window.hlsPlayer_destroyInstance=function(t){const e=window.hlsPlayer_instances[t];e&&(delete window.hlsPlayer_instances[t],e.video.pause(),e.hls.destroy())},window.bridgeInvokeCallback=function(t,e){const s=window.bridgeCallbackMap[t];s&&s(e)}})(); \ No newline at end of file diff --git a/submodules/TelegramUniversalVideoContent/HlsBundle/index.html b/submodules/TelegramUniversalVideoContent/HlsBundle/index.html new file mode 100644 index 0000000000..73cff466df --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/HlsBundle/index.html @@ -0,0 +1 @@ +Production \ No newline at end of file diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/.gitignore b/submodules/TelegramUniversalVideoContent/PlayerSource/.gitignore new file mode 100644 index 0000000000..fcd6fcc3a8 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/.gitignore @@ -0,0 +1,28 @@ +# Node modules +node_modules/ + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Webpack and build artifacts +/dist +/build + +# Environment files +.env +.env.local +.env.*.local + +# OS generated +.DS_Store +Thumbs.db diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/.vscode/launch.json b/submodules/TelegramUniversalVideoContent/PlayerSource/.vscode/launch.json new file mode 100644 index 0000000000..b8d2d8476e --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/.vscode/launch.json @@ -0,0 +1,8 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + ] +} \ No newline at end of file diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/build.sh b/submodules/TelegramUniversalVideoContent/PlayerSource/build.sh new file mode 100644 index 0000000000..0fbfb5b637 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/build.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +rm -rf ../HlsBundle +mkdir ../HlsBundle +npm run build-$1 +cp ./dist/* ../HlsBundle/ diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/package-lock.json b/submodules/TelegramUniversalVideoContent/PlayerSource/package-lock.json new file mode 100644 index 0000000000..6cb6a5f8b8 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/package-lock.json @@ -0,0 +1,4461 @@ +{ + "name": "myhls", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "myhls", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "hls.js": "^1.5.15" + }, + "devDependencies": { + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "expose-loader": "^5.0.0", + "express": "^4.18.2", + "html-webpack-plugin": "^5.5.3", + "style-loader": "^3.3.3", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "webpack-dev-middleware": "^6.1.1", + "webpack-dev-server": "^4.15.1", + "webpack-merge": "^6.0.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001666", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", + "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.31", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz", + "integrity": "sha512-QcDoBbQeYt0+3CWcK/rEbuHvwpbT/8SV9T3OSgs6cX1FlcUAkgrkqbg9zLnDrMM/rLamzQwal4LYFCiWk861Tg==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expose-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", + "integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", + "dev": true, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/express": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hls.js": { + "version": "1.5.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.15.tgz", + "integrity": "sha512-6cD7xN6bycBHaXz2WyPIaHn/iXFizE5au2yvY5q9aC4wfihxAr16C9fUy4nxh2a3wOw0fEgLRa9dN6wsYjlpNg==" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webpack": { + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-cli/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", + "integrity": "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/package.json b/submodules/TelegramUniversalVideoContent/PlayerSource/package.json new file mode 100644 index 0000000000..00b6d56091 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/package.json @@ -0,0 +1,30 @@ +{ + "name": "myhls", + "version": "1.0.0", + "description": "", + "private": true, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build-development": "webpack --config webpack.dev.js", + "build-release": "webpack --config webpack.prod.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "expose-loader": "^5.0.0", + "express": "^4.18.2", + "html-webpack-plugin": "^5.5.3", + "style-loader": "^3.3.3", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "webpack-dev-middleware": "^6.1.1", + "webpack-dev-server": "^4.15.1", + "webpack-merge": "^6.0.1" + }, + "dependencies": { + "hls.js": "^1.5.15" + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/server.js b/submodules/TelegramUniversalVideoContent/PlayerSource/server.js new file mode 100644 index 0000000000..66f8abbd63 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/server.js @@ -0,0 +1,20 @@ +const express = require('express'); +const webpack = require('webpack'); +const webpackDevMiddleware = require('webpack-dev-middleware'); + +const app = express(); +const config = require('./webpack.config.js'); +const compiler = webpack(config); + +// Tell express to use the webpack-dev-middleware and use the webpack.config.js +// configuration file as a base. +app.use( + webpackDevMiddleware(compiler, { + publicPath: config.output.publicPath, + }) +); + +// Serve the files on port 3000. +app.listen(3000, function () { + console.log('Example app listening on port 3000!\n'); +}); diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/MediaSourceStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/MediaSourceStub.js new file mode 100644 index 0000000000..b12a553a63 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/MediaSourceStub.js @@ -0,0 +1,240 @@ +import { TimeRangesStub } from "./TimeRangesStub.js" + +function bytesToBase64(bytes) { + const binString = Array.from(bytes, (byte) => + String.fromCodePoint(byte), + ).join(""); + return btoa(binString); +} + +export class SourceBufferListStub extends EventTarget { + constructor() { + super(); + this._buffers = []; + } + + _add(buffer) { + this._buffers.push(buffer); + this.dispatchEvent(new Event('addsourcebuffer')); + } + + _remove(buffer) { + const index = this._buffers.indexOf(buffer); + if (index === -1) { + return false; + } + this._buffers.splice(index, 1); + this.dispatchEvent(new Event('removesourcebuffer')); + return true; + } + + get length() { + return this._buffers.length; + } + + item(index) { + return this._buffers[index]; + } + + [Symbol.iterator]() { + return this._buffers[Symbol.iterator](); + } +} + +export class SourceBufferStub extends EventTarget { + constructor(mediaSource, mimeType) { + super(); + this.mediaSource = mediaSource; + this.mimeType = mimeType; + this.updating = false; + this.buffered = new TimeRangesStub(); + this.timestampOffset = 0; + this.appendWindowStart = 0; + this.appendWindowEnd = Infinity; + + this.bridgeId = window.nextInternalId; + window.nextInternalId += 1; + window.bridgeObjectMap[this.bridgeId] = this; + + window.bridgeInvokeAsync(this.bridgeId, "SourceBuffer", "constructor", { + "mediaSourceId": this.mediaSource.bridgeId, + "mimeType": mimeType + }); + } + + appendBuffer(data) { + if (this.updating) { + throw new DOMException('SourceBuffer is updating', 'InvalidStateError'); + } + this.updating = true; + this.dispatchEvent(new Event('updatestart')); + + window.bridgeInvokeAsync(this.bridgeId, "SourceBuffer", "appendBuffer", { + "data": bytesToBase64(data) + }).then((result) => { + const updatedRanges = result["ranges"]; + var ranges = []; + for (var i = 0; i < updatedRanges.length; i += 2) { + ranges.push({ + start: updatedRanges[i], + end: updatedRanges[i + 1] + }); + } + this.buffered._ranges = ranges; + + this.mediaSource._reopen(); + this.mediaSource.emitUpdatedBuffer(); + + this.updating = false; + this.dispatchEvent(new Event('update')); + this.dispatchEvent(new Event('updateend')); + }); + } + + abort() { + if (this.updating) { + this.updating = false; + this.dispatchEvent(new Event('abort')); + + window.bridgeInvokeAsync(this.bridgeId, "SourceBuffer", "abort", {}).then((result) => { + }); + } + } + + remove(start, end) { + if (this.updating) { + throw new DOMException('SourceBuffer is updating', 'InvalidStateError'); + } + this.updating = true; + this.dispatchEvent(new Event('updatestart')); + + window.bridgeInvokeAsync(this.bridgeId, "SourceBuffer", "remove", { + "start": start, + "end": end + }).then((result) => { + const updatedRanges = result["ranges"]; + var ranges = []; + for (var i = 0; i < updatedRanges.length; i += 2) { + ranges.push({ + start: updatedRanges[i], + end: updatedRanges[i + 1] + }); + } + this.buffered._ranges = ranges; + + this.mediaSource._reopen(); + this.mediaSource.emitUpdatedBuffer(); + + this.updating = false; + this.dispatchEvent(new Event('update')); + this.dispatchEvent(new Event('updateend')); + }); + } +} + +export class MediaSourceStub extends EventTarget { + constructor() { + super(); + + this.internalId = window.nextInternalId; + window.nextInternalId += 1; + + this.bridgeId = window.nextInternalId; + window.nextInternalId += 1; + window.bridgeObjectMap[this.bridgeId] = this; + + this.sourceBuffers = new SourceBufferListStub(); + this.activeSourceBuffers = new SourceBufferListStub(); + this.readyState = 'closed'; + this._duration = NaN; + + window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "constructor", { + "id": this.internalId + }); + + // Simulate asynchronous opening of MediaSource + setTimeout(() => { + this.readyState = 'open'; + this.dispatchEvent(new Event('sourceopen')); + }, 0); + } + + static isTypeSupported(mimeType) { + // Assume all MIME types are supported in this stub + return true; + } + + emitUpdatedBuffer() { + this.dispatchEvent(new Event("bufferChanged")); + } + + getBufferedRanges() { + if (this.sourceBuffers._buffers.length != 0) { + return this.sourceBuffers._buffers[0].buffered._ranges; + } + return []; + } + + addSourceBuffer(mimeType) { + if (this.readyState !== 'open') { + throw new DOMException('MediaSource is not open', 'InvalidStateError'); + } + const sourceBuffer = new SourceBufferStub(this, mimeType); + this.sourceBuffers._add(sourceBuffer); + this.activeSourceBuffers._add(sourceBuffer); + + this.dispatchEvent(new Event("bufferChanged")); + + window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "updateSourceBuffers", { + "ids": this.sourceBuffers._buffers.map((sb) => sb.bridgeId) + }).then((result) => { + }) + + return sourceBuffer; + } + + removeSourceBuffer(sourceBuffer) { + if (!this.sourceBuffers._remove(sourceBuffer)) { + throw new DOMException('SourceBuffer not found', 'NotFoundError'); + } + this.activeSourceBuffers._remove(sourceBuffer); + + this.dispatchEvent(new Event("bufferChanged")); + + window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "updateSourceBuffers", { + "ids": this.sourceBuffers._buffers.map((sb) => sb.bridgeId) + }).then((result) => { + }) + } + + endOfStream(error) { + if (this.readyState !== 'open') { + throw new DOMException('MediaSource is not open', 'InvalidStateError'); + } + this.readyState = 'ended'; + this.dispatchEvent(new Event('sourceended')); + } + + _reopen() { + if (this.readyState !== 'open') { + this.readyState = 'open'; + this.dispatchEvent(new Event('sourceopen')); + } + } + + set duration(value) { + if (this.readyState === 'closed') { + throw new DOMException('MediaSource is closed', 'InvalidStateError'); + } + this._duration = value; + + window.bridgeInvokeAsync(this.bridgeId, "MediaSource", "setDuration", { + "duration": value + }).then((result) => { + }) + } + + get duration() { + return this._duration; + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/TextTrackStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/TextTrackStub.js new file mode 100644 index 0000000000..e9aebfc22e --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/TextTrackStub.js @@ -0,0 +1,85 @@ + +export class TextTrackStub extends EventTarget { + constructor(kind = '', label = '', language = '') { + super(); + this.kind = kind; + this.label = label; + this.language = language; + this.mode = 'disabled'; // 'disabled', 'hidden', or 'showing' + this.cues = new TextTrackCueListStub(); + this.activeCues = new TextTrackCueListStub(); + } + + addCue(cue) { + this.cues._add(cue); + } + + removeCue(cue) { + this.cues._remove(cue); + } +} + +export class TextTrackCueListStub { + constructor() { + this._cues = []; + } + + get length() { + return this._cues.length; + } + + item(index) { + return this._cues[index]; + } + + getCueById(id) { + return this._cues.find(cue => cue.id === id) || null; + } + + _add(cue) { + this._cues.push(cue); + } + + _remove(cue) { + const index = this._cues.indexOf(cue); + if (index !== -1) { + this._cues.splice(index, 1); + } + } + + [Symbol.iterator]() { + return this._cues[Symbol.iterator](); + } +} + +export class TextTrackListStub extends EventTarget { + constructor() { + super(); + this._tracks = []; + } + + get length() { + return this._tracks.length; + } + + item(index) { + return this._tracks[index]; + } + + _add(track) { + this._tracks.push(track); + this.dispatchEvent(new Event('addtrack')); + } + + _remove(track) { + const index = this._tracks.indexOf(track); + if (index !== -1) { + this._tracks.splice(index, 1); + this.dispatchEvent(new Event('removetrack')); + } + } + + [Symbol.iterator]() { + return this._tracks[Symbol.iterator](); + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/TimeRangesStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/TimeRangesStub.js new file mode 100644 index 0000000000..e8a9df021e --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/TimeRangesStub.js @@ -0,0 +1,74 @@ + +export class TimeRangesStub { + constructor() { + this._ranges = []; + } + + get length() { + return this._ranges.length; + } + + start(index) { + if (index < 0 || index >= this._ranges.length) { + throw new DOMException('Invalid index', 'IndexSizeError'); + } + return this._ranges[index].start; + } + + end(index) { + if (index < 0 || index >= this._ranges.length) { + throw new DOMException('Invalid index', 'IndexSizeError'); + } + return this._ranges[index].end; + } + + // Helper method to add a range + _addRange(start, end) { + this._ranges.push({ start, end }); + this._normalizeRanges(); + } + + // Helper method to remove ranges that overlap with a given range + _removeRange(start, end) { + let updatedRanges = []; + for (let range of this._ranges) { + if (range.end <= start || range.start >= end) { + // No overlap, keep the range as is + updatedRanges.push(range); + } else if (range.start < start && range.end > end) { + // The range fully covers the removal range, split into two ranges + updatedRanges.push({ start: range.start, end: start }); + updatedRanges.push({ start: end, end: range.end }); + } else if (range.start >= start && range.end <= end) { + // The range is entirely within the removal range, remove it + // Do not add to updatedRanges + } else if (range.start < start && range.end > start && range.end <= end) { + // The range overlaps with the removal range on the left + updatedRanges.push({ start: range.start, end: start }); + } else if (range.start >= start && range.start < end && range.end > end) { + // The range overlaps with the removal range on the right + updatedRanges.push({ start: end, end: range.end }); + } + } + this._ranges = updatedRanges; + } + + // Normalize and merge overlapping ranges + _normalizeRanges() { + this._ranges.sort((a, b) => a.start - b.start); + let normalized = []; + for (let range of this._ranges) { + if (normalized.length === 0) { + normalized.push(range); + } else { + let last = normalized[normalized.length - 1]; + if (range.start <= last.end) { + last.end = Math.max(last.end, range.end); + } else { + normalized.push(range); + } + } + } + this._ranges = normalized; + } +} \ No newline at end of file diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/VideoElementStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/VideoElementStub.js new file mode 100644 index 0000000000..cd59430c34 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/VideoElementStub.js @@ -0,0 +1,196 @@ +import { TimeRangesStub } from "./TimeRangesStub.js" +import { TextTrackStub, TextTrackListStub } from "./TextTrackStub.js" + +export class VideoElementStub extends EventTarget { + constructor(id) { + super(); + + this.instanceId = id; + + this.bridgeId = window.nextInternalId; + window.nextInternalId += 1; + window.bridgeObjectMap[this.bridgeId] = this; + + this._currentTime = 0.0; + this.duration = NaN; + this.paused = true; + this._playbackRate = 1.0; + this.volume = 1.0; + this.muted = false; + this.readyState = 0; + this.networkState = 0; + this.buffered = new TimeRangesStub(); + this.seeking = false; + this.loop = false; + this.autoplay = false; + this.controls = false; + this.error = null; + this._src = ''; + this.videoWidth = 0; + this.videoHeight = 0; + this.textTracks = new TextTrackListStub(); + this.isWaiting = false; + this.currentMedia = null; + + window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "constructor", { + "instanceId": this.instanceId + }); + + setTimeout(() => { + this.readyState = 4; // HAVE_ENOUGH_DATA + this.dispatchEvent(new Event('loadedmetadata')); + this.dispatchEvent(new Event('loadeddata')); + this.dispatchEvent(new Event('canplay')); + this.dispatchEvent(new Event('canplaythrough')); + }, 0); + } + + get currentTime() { + return this._currentTime; + } + + set currentTime(value) { + if (this._currentTime != value) { + this._currentTime = value; + + this.dispatchEvent(new Event('seeking')); + + window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setCurrentTime", { + "instanceId": this.instanceId, + "currentTime": value + }).then((result) => { + this.dispatchEvent(new Event('seeked')); + }) + } + } + + get playbackRate() { + return this._playbackRate; + } + + set playbackRate(value) { + this._playbackRate = value; + + window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setPlaybackRate", { + "instanceId": this.instanceId, + "playbackRate": value + }).then((result) => { + }) + } + + get src() { + return this._src; + } + + set src(value) { + if (this.currentMedia) { + this.currentMedia.removeEventListener("bufferChanged", false); + } + + this._src = value; + var media = window.mediaSourceMap[this._src]; + this.currentMedia = media; + if (media) { + media.addEventListener("bufferChanged", () => { + this.updateBufferedFromMediaSource(); + }, false); + window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "setMediaSource", { + "instanceId": this.instanceId, + "mediaSourceId": media.bridgeId + }).then((result) => { + }) + } + } + + removeAttribute(name) { + if (name === "src") { + this._src = ""; + } + } + + querySelectorAll(name) { + const fragment = document.createDocumentFragment(); + return fragment.querySelectorAll('*'); + } + + updateBufferedFromMediaSource() { + var currentMedia = this.currentMedia; + if (currentMedia) { + this.buffered._ranges = currentMedia.getBufferedRanges(); + } else { + this.buffered._ranges = []; + } + } + + bridgeUpdateStatus(dict) { + var paused = !dict["isPlaying"]; + var isWaiting = dict["isWaiting"]; + var currentTime = dict["currentTime"]; + + if (this.paused != paused) { + this.paused = paused; + + if (paused) { + this.dispatchEvent(new Event('pause')); + } else { + this.dispatchEvent(new Event('play')); + this.dispatchEvent(new Event('playing')); + } + } + + if (this.isWaiting != isWaiting) { + this.isWaiting = isWaiting; + if (isWaiting) { + this.dispatchEvent(new Event('waiting')); + } + } + + if (this._currentTime != currentTime) { + this._currentTime = currentTime; + this.dispatchEvent(new Event('timeupdate')); + } + } + + play() { + if (this.paused) { + return window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "play", { + "instanceId": this.instanceId, + }).then((result) => { + this.dispatchEvent(new Event('play')); + this.dispatchEvent(new Event('playing')); + }) + } else { + return Promise.resolve(); + } + } + + pause() { + if (!this.paused) { + this.paused = true; + this.dispatchEvent(new Event('pause')); + + return window.bridgeInvokeAsync(this.bridgeId, "VideoElement", "pause", { + "instanceId": this.instanceId, + }).then((result) => { + }) + } + } + + canPlayType(type) { + return 'probably'; + } + + addTextTrack(kind, label, language) { + const textTrack = new TextTrackStub(kind, label, language); + this.textTracks._add(textTrack); + return textTrack; + } + + load() { + } + + notifySeeked() { + this.dispatchEvent(new Event('seeking')); + this.dispatchEvent(new Event('seeked')); + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/XMLHttpRequestStub.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/XMLHttpRequestStub.js new file mode 100644 index 0000000000..c79dd489bb --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/XMLHttpRequestStub.js @@ -0,0 +1,150 @@ + +function base64ToArrayBuffer(base64) { + var binaryString = atob(base64); + var bytes = new Uint8Array(binaryString.length); + for (var i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} + +export class XMLHttpRequestStub extends EventTarget { + constructor() { + super(); + + this.bridgeId = window.nextInternalId; + window.nextInternalId += 1; + + this.readyState = 0; + this.status = 0; + this.statusText = ""; + this.responseText = ""; + this.responseXML = null; + this._responseData = null; + this.onreadystatechange = null; + this._requestHeaders = {}; + this._responseHeaders = {}; + this._method = ""; + this._url = ""; + this._async = true; + this._user = null; + this._password = null; + this._responseType = ""; + } + + open(method, url, async = true, user = null, password = null) { + this._method = method; + this._url = url; + this._async = async; + this._user = user; + this._password = password; + this.readyState = 1; // Opened + this._triggerReadyStateChange(); + } + + setRequestHeader(header, value) { + this._requestHeaders[header] = value; + } + + getResponseHeader(header) { + return this._responseHeaders[header.toLowerCase()] || null; + } + + getAllResponseHeaders() { + return Object.entries(this._responseHeaders) + .map(([header, value]) => `${header}: ${value}`) + .join('\r\n'); + } + + send(body = null) { + this.readyState = 2; + this._triggerReadyStateChange(); + + this.readyState = 3; // Loading + this._triggerReadyStateChange(); + + this.dispatchEvent(new Event("loadstart")); + + window.bridgeInvokeAsync(this.bridgeId, "XMLHttpRequest", "load", { + "id": this.bridgeId, + "url": this._url, + "requestHeaders": this._requestHeaders + }).then((result) => { + if (result["error"]) { + this.dispatchEvent(new Event("error")); + } else { + this.status = result["status"]; + this.statusText = result["statusText"]; + + if (result["responseData"]) { + if (this._responseType === "arraybuffer") { + this._responseData = base64ToArrayBuffer(result["responseData"]); + } else { + this.responseText = atob(result["responseData"]); + } + this.responseXML = null; + } else { + this.response = null; + this.responseText = result["responseText"] || null; + this.responseXML = result["responseXML"] || null; + } + this._responseHeaders = result["responseHeaders"]; + + this.readyState = 4; // Done + this._triggerReadyStateChange(); + + this.dispatchEvent(new Event("load")); + } + + this.dispatchEvent(new Event("loadend")); + }); + } + + abort() { + this.dispatchEvent(new Event("abort")); + + window.bridgeInvokeAsync(this.bridgeId, "XMLHttpRequest", "abort", { + "id": this.bridgeId + }); + this.readyState = 0; + this.status = 0; + this.statusText = ''; + this.responseText = ''; + this.responseXML = null; + this._responseHeaders = {}; + this._triggerReadyStateChange(); + } + + overrideMimeType(mime) { + } + + set responseType(type) { + this._responseType = type; + } + + get responseType() { + return this._responseType; + } + + get response() { + if (this._responseType === '' || this._responseType === 'text') { + return this.responseText; + } + return this._responseData; + } + + _triggerReadyStateChange() { + this.dispatchEvent(new Event('readystatechange')); + if (typeof this.onreadystatechange === 'function') { + this.onreadystatechange(); + } + } + + // Additional methods to simulate responses + _setResponse(status, statusText, responseText, responseHeaders = {}) { + this.status = status; + this.statusText = statusText; + this.responseText = responseText; + this._responseHeaders = responseHeaders; + } +} diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/index.js b/submodules/TelegramUniversalVideoContent/PlayerSource/src/index.js new file mode 100644 index 0000000000..1df18a87b0 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/index.js @@ -0,0 +1,238 @@ +import Hls from "hls.js"; +import { VideoElementStub } from "./VideoElementStub.js" +import { MediaSourceStub, SourceBufferStub } from "./MediaSourceStub.js" +import { XMLHttpRequestStub } from "./XMLHttpRequestStub.js" + +window.bridgeObjectMap = {}; +window.bridgeCallbackMap = {}; + +function bridgeInvokeAsync(bridgeId, className, methodName, params) { + var promiseResolve; + var promiseReject; + var result = new Promise(function(resolve, reject) { + promiseResolve = resolve; + promiseReject = reject; + }); + const callbackId = window.nextInternalId; + window.nextInternalId += 1; + window.bridgeCallbackMap[callbackId] = promiseResolve; + + if (window.webkit.messageHandlers) { + window.webkit.messageHandlers.performAction.postMessage({ + 'event': 'bridgeInvoke', + 'data': { + 'bridgeId': bridgeId, + 'className': className, + 'methodName': methodName, + 'params': params, + 'callbackId': callbackId + } + }); + } + + return result; +} +window.bridgeInvokeAsync = bridgeInvokeAsync + +export function bridgeInvokeCallback(callbackId, result) { + const callback = window.bridgeCallbackMap[callbackId]; + if (callback) { + callback(result); + } +} + +window.nextInternalId = 0; +window.mediaSourceMap = {}; + +// Replace the global MediaSource with our stub +if (typeof window !== 'undefined') { + window.MediaSource = MediaSourceStub; + window.ManagedMediaSource = MediaSourceStub; + window.SourceBuffer = SourceBufferStub; + window.XMLHttpRequest = XMLHttpRequestStub; + + URL.createObjectURL = function(ms) { + const url = "blob:mock-media-source:" + ms.internalId; + window.mediaSourceMap[url] = ms; + return url; + }; +} + +function postPlayerEvent(id, eventName, eventData) { + if (window.webkit && window.webkit.messageHandlers) { + window.webkit.messageHandlers.performAction.postMessage({'instanceId': id, 'event': eventName, 'data': eventData}); + } +} + +export class HlsPlayerInstance { + constructor(id) { + this.id = id; + this.isManifestParsed = false; + this.currentTimeUpdateTimeout = null; + this.notifySeekedOnNextStatusUpdate = false; + this.video = new VideoElementStub(this.id); + } + + playerInitialize(params) { + this.video.addEventListener("playing", () => { + this.refreshPlayerStatus(); + }); + this.video.addEventListener("pause", () => { + this.refreshPlayerStatus(); + }); + this.video.addEventListener("seeking", () => { + this.refreshPlayerStatus(); + }); + this.video.addEventListener("waiting", () => { + this.refreshPlayerStatus(); + }); + + this.hls = new Hls({ + startLevel: 0, + testBandwidth: false, + debug: params['debug'] || true, + autoStartLoad: false, + backBufferLength: 30, + maxBufferLength: 60, + maxMaxBufferLength: 60, + maxFragLookUpTolerance: 0.001, + nudgeMaxRetry: 10000 + }); + this.hls.on(Hls.Events.MANIFEST_PARSED, () => { + this.isManifestParsed = true; + this.refreshPlayerStatus(); + }); + + this.hls.on(Hls.Events.LEVEL_SWITCHED, () => { + this.refreshPlayerStatus(); + }); + this.hls.on(Hls.Events.LEVELS_UPDATED, () => { + this.refreshPlayerStatus(); + }); + + this.hls.loadSource(params["urlPrefix"] + "master.m3u8"); + this.hls.attachMedia(this.video); + } + + playerLoad(initialLevelIndex) { + this.hls.startLevel = initialLevelIndex; + this.hls.startLoad(-1, false); + } + + playerPlay() { + this.video.play(); + } + + playerPause() { + this.video.pause(); + } + + playerSetBaseRate(value) { + this.video.playbackRate = value; + } + + playerSetLevel(level) { + if (level >= 0) { + this.hls.currentLevel = level; + } else { + this.hls.currentLevel = -1; + } + } + + playerSeek(value) { + this.video.currentTime = value; + } + + playerSetIsMuted(value) { + this.video.muted = value; + } + + getLevels() { + var levels = []; + for (var i = 0; i < this.hls.levels.length; i++) { + var level = this.hls.levels[i]; + levels.push({ + 'index': i, + 'bitrate': level.bitrate || 0, + 'width': level.width || 0, + 'height': level.height || 0 + }); + } + return levels; + } + + refreshPlayerStatus() { + var isPlaying = false; + if (!this.video.paused && !this.video.ended && this.video.readyState > 2) { + isPlaying = true; + } + + postPlayerEvent(this.id, 'playerStatus', { + 'isReady': this.isManifestParsed, + 'isPlaying': !this.video.paused, + 'rate': isPlaying ? this.video.playbackRate : 0.0, + 'defaultRate': this.video.playbackRate, + 'levels': this.getLevels(), + 'currentLevel': this.hls.currentLevel + }); + + this.refreshPlayerCurrentTime(); + + if (isPlaying) { + if (this.currentTimeUpdateTimeout == null) { + this.currentTimeUpdateTimeout = setTimeout(() => { + this.refreshPlayerCurrentTime(); + }, 200); + } + } else { + if(this.currentTimeUpdateTimeout != null){ + clearTimeout(this.currentTimeUpdateTimeout); + this.currentTimeUpdateTimeout = null; + } + } + + if (this.notifySeekedOnNextStatusUpdate) { + this.notifySeekedOnNextStatusUpdate = false; + this.video.notifySeeked(); + } + } + + playerNotifySeekedOnNextStatusUpdate() { + this.notifySeekedOnNextStatusUpdate = true; + } + + refreshPlayerCurrentTime() { + postPlayerEvent(this.id, 'playerCurrentTime', { + 'value': this.video.currentTime + }); + this.currentTimeUpdateTimeout = setTimeout(() => { + this.refreshPlayerCurrentTime() + }, 200); + } +} + +window.invokeOnLoad = function() { + postPlayerEvent(this.id, 'windowOnLoad', { + }); +} + +window.onload = () => { + window.invokeOnLoad(); +}; + +window.hlsPlayer_instances = {}; + +window.hlsPlayer_makeInstance = function(id) { + window.hlsPlayer_instances[id] = new HlsPlayerInstance(id); +} + +window.hlsPlayer_destroyInstance = function(id) { + const instance = window.hlsPlayer_instances[id]; + if (instance) { + delete window.hlsPlayer_instances[id]; + instance.video.pause(); + instance.hls.destroy(); + } +} + +window.bridgeInvokeCallback = bridgeInvokeCallback; diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/src/style.css b/submodules/TelegramUniversalVideoContent/PlayerSource/src/style.css new file mode 100644 index 0000000000..4cf22426dc --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/src/style.css @@ -0,0 +1,15 @@ +html, body { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + overflow: hidden; +} +video { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + object-fit: fill; +} + diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.common.js b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.common.js new file mode 100644 index 0000000000..a8d8d45fc7 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.common.js @@ -0,0 +1,35 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: { + index: './src/index.js', + }, + plugins: [ + new HtmlWebpackPlugin({ + title: 'Production', + scriptLoading: 'blocking', + }) + ], + output: { + filename: '[name].bundle.js', + path: path.resolve(__dirname, 'dist'), + clean: true, + publicPath: '', + }, + module: { + rules: [ + { + test: /\.js$/, + include: path.resolve(__dirname, 'src/index.js'), + }, + { + test: /\.css$/i, + use: [ + 'style-loader', + 'css-loader' + ], + }, + ], + }, +}; diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.dev.js b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.dev.js new file mode 100644 index 0000000000..bf02fafa86 --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.dev.js @@ -0,0 +1,10 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); + +module.exports = merge(common, { + mode: 'development', + devtool: 'source-map', + devServer: { + static: './dist', + }, +}); diff --git a/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.prod.js b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.prod.js new file mode 100644 index 0000000000..6266c6035f --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/PlayerSource/webpack.prod.js @@ -0,0 +1,15 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); +const TerserPlugin = require('terser-webpack-plugin'); + +module.exports = merge(common, { + mode: 'production', + optimization: { + minimize: true, + minimizer: [new TerserPlugin({ + terserOptions: { + compress: true, + }, + })], + }, +}); diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoAVContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoAVContentNode.swift deleted file mode 100644 index a47dc9becc..0000000000 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoAVContentNode.swift +++ /dev/null @@ -1,549 +0,0 @@ -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() - var status: Signal { - return self._status.get() - } - - private let _bufferingStatus = Promise<(RangeSet, Int64)?>() - var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { - return self._bufferingStatus.get() - } - - var isNativePictureInPictureActive: Signal { - return .single(false) - } - - private let _ready = Promise() - var ready: Signal { - return self._ready.get() - } - - private let _preloadCompleted = ValuePromise() - var preloadCompleted: Signal { - 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()) - - 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 ?? "")") -#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() { - } -} diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift index de44e0b2c6..27942f6050 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoContent.swift @@ -26,7 +26,7 @@ public final class HLSQualitySet { for attribute in alternativeFile.attributes { if case let .Video(_, size, _, _, _, videoCodec) = attribute { if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) { - let key = Int(size.height) + let key = Int(min(size.width, size.height)) if let currentFile = qualityFiles[key] { var currentCodec: String? for attribute in currentFile.media.attributes { @@ -214,11 +214,7 @@ public final class HLSVideoContent: UniversalVideoContent { } public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - if #available(iOS 17.1, *) { - return HLSVideoJSContentNode(accountId: accountId, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) - } else { - return HLSVideoAVContentNode(accountId: accountId, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) - } + return HLSVideoJSNativeContentNode(accountId: accountId, postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically) } public func isEqual(to other: UniversalVideoContent) -> Bool { diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSContentNode.swift deleted file mode 100644 index 9e82785f4b..0000000000 --- a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSContentNode.swift +++ /dev/null @@ -1,777 +0,0 @@ -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? { - 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..(value: false) - var disposable: Disposable? - - init(source: HLSServerSource, sourceTask: any WKURLSchemeTask) { - self.sourceTask = sourceTask - - var requestRange: Range? - 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? = 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() - var status: Signal { - return self._status.get() - } - - private let _bufferingStatus = Promise<(RangeSet, Int64)?>() - var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { - return self._bufferingStatus.get() - } - - private let _isNativePictureInPictureActive = ValuePromise(false, ignoreRepeated: true) - var isNativePictureInPictureActive: Signal { - return self._isNativePictureInPictureActive.get() - } - - private let _ready = Promise() - var ready: Signal { - return self._ready.get() - } - - private let _preloadCompleted = ValuePromise() - var preloadCompleted: Signal { - 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()) - - 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) - } -} diff --git a/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift new file mode 100644 index 0000000000..9d89d4112b --- /dev/null +++ b/submodules/TelegramUniversalVideoContent/Sources/HLSVideoJSNativeContentNode.swift @@ -0,0 +1,1847 @@ +import Foundation +import AVFoundation +import SwiftSignalKit +import UniversalMediaPlayer +import Postbox +import TelegramCore +import WebKit +import AsyncDisplayKit +import AccountContext +import TelegramAudio +import Display +import PhotoResources +import TelegramVoip +import RangeSet +import AppBundle +import ManagedFile +import FFMpegBinding +import RangeSet + +private func parseRange(from rangeString: String) -> Range? { + 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 +} + +final class HLSJSServerSource: SharedHLSServer.Source { + let id: String + let postbox: Postbox + let userLocation: MediaResourceUserLocation + let playlistFiles: [Int: FileMediaReference] + let qualityFiles: [Int: FileMediaReference] + + private var playlistFetchDisposables: [Int: Disposable] = [:] + + init(accountId: Int64, fileId: Int64, postbox: Postbox, userLocation: MediaResourceUserLocation, playlistFiles: [Int: FileMediaReference], qualityFiles: [Int: FileMediaReference]) { + self.id = "\(UInt64(bitPattern: accountId))_\(fileId)" + self.postbox = postbox + self.userLocation = userLocation + self.playlistFiles = playlistFiles + self.qualityFiles = qualityFiles + } + + deinit { + for (_, disposable) in self.playlistFetchDisposables { + disposable.dispose() + } + } + + func arbitraryFileData(path: String) -> Signal<(data: Data, contentType: String)?, NoError> { + return Signal { subscriber in + let bundle = Bundle(for: HLSJSServerSource.self) + + let bundlePath = bundle.bundlePath + "/HlsBundle.bundle" + if let data = try? Data(contentsOf: URL(fileURLWithPath: bundlePath + "/" + path)) { + let mimeType: String + let pathExtension = (path as NSString).pathExtension + if pathExtension == "html" { + mimeType = "text/html" + } else if pathExtension == "html" { + mimeType = "application/javascript" + } else { + mimeType = "application/octet-stream" + } + subscriber.putNext((data, mimeType)) + } else { + subscriber.putNext(nil) + } + + subscriber.putCompletion() + + return EmptyDisposable + } + } + + func masterPlaylistData() -> Signal { + var playlistString: String = "" + playlistString.append("#EXTM3U\n") + + for (quality, file) in self.qualityFiles.sorted(by: { $0.key > $1.key }) { + let width = file.media.dimensions?.width ?? 1280 + let height = file.media.dimensions?.height ?? 720 + + let bandwidth: Int + if let size = file.media.size, let duration = file.media.duration, duration != 0.0 { + bandwidth = Int(Double(size) / duration) * 8 + } else { + bandwidth = 1000000 + } + + playlistString.append("#EXT-X-STREAM-INF:BANDWIDTH=\(bandwidth),RESOLUTION=\(width)x\(height)\n") + playlistString.append("hls_level_\(quality).m3u8\n") + } + return .single(playlistString) + } + + func playlistData(quality: Int) -> Signal { + guard let playlistFile = self.playlistFiles[quality] else { + return .never() + } + if self.playlistFetchDisposables[quality] == nil { + self.playlistFetchDisposables[quality] = freeMediaFileResourceInteractiveFetched(postbox: self.postbox, userLocation: self.userLocation, fileReference: playlistFile, resource: playlistFile.media.resource).startStrict() + } + + return self.postbox.mediaBox.resourceData(playlistFile.media.resource) + |> filter { data in + return data.complete + } + |> map { data -> String in + guard data.complete else { + return "" + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { + return "" + } + guard var playlistString = String(data: data, encoding: .utf8) else { + return "" + } + let partRegex = try! NSRegularExpression(pattern: "mtproto:([\\d]+)", options: []) + let results = partRegex.matches(in: playlistString, range: NSRange(playlistString.startIndex..., in: playlistString)) + for result in results.reversed() { + if let range = Range(result.range, in: playlistString) { + if let fileIdRange = Range(result.range(at: 1), in: playlistString) { + let fileId = String(playlistString[fileIdRange]) + playlistString.replaceSubrange(range, with: "partfile\(fileId).mp4") + } + } + } + return playlistString + } + } + + func partData(index: Int, quality: Int) -> Signal { + return .never() + } + + func fileData(id: Int64, range: Range) -> Signal<(TempBoxFile, Range, Int)?, NoError> { + guard let (quality, file) = self.qualityFiles.first(where: { $0.value.media.fileId.id == id }) else { + return .single(nil) + } + let _ = quality + guard let size = file.media.size else { + return .single(nil) + } + + let postbox = self.postbox + let userLocation = self.userLocation + + let mappedRange: Range = Int64(range.lowerBound) ..< Int64(range.upperBound) + + let queue = postbox.mediaBox.dataQueue + let fetchFromRemote: Signal<(TempBoxFile, Range, Int)?, NoError> = Signal { subscriber in + let partialFile = TempBox.shared.tempFile(fileName: "data") + + if let cachedData = postbox.mediaBox.internal_resourceData(id: file.media.resource.id, size: size, in: Int64(range.lowerBound) ..< Int64(range.upperBound)) { + #if DEBUG + print("Fetched \(quality)p part from cache") + #endif + + let outputFile = ManagedFile(queue: nil, path: partialFile.path, mode: .readwrite) + if let outputFile { + let blockSize = 128 * 1024 + var tempBuffer = Data(count: blockSize) + var blockOffset = 0 + while blockOffset < cachedData.length { + let currentBlockSize = min(cachedData.length - blockOffset, blockSize) + + tempBuffer.withUnsafeMutableBytes { bytes -> Void in + let _ = cachedData.file.read(bytes.baseAddress!, currentBlockSize) + let _ = outputFile.write(bytes.baseAddress!, count: currentBlockSize) + } + + blockOffset += blockSize + } + outputFile._unsafeClose() + subscriber.putNext((partialFile, 0 ..< cachedData.length, Int(size))) + subscriber.putCompletion() + } else { + #if DEBUG + print("Error writing cached file to disk") + #endif + } + + return EmptyDisposable + } + + guard let fetchResource = postbox.mediaBox.fetchResource else { + return EmptyDisposable + } + + let location = MediaResourceStorageLocation(userLocation: userLocation, reference: file.resourceReference(file.media.resource)) + let params = MediaResourceFetchParameters( + tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), + info: TelegramCloudMediaResourceFetchInfo(reference: file.resourceReference(file.media.resource), preferBackgroundReferenceRevalidation: true, continueInBackground: true), + location: location, + contentType: .video, + isRandomAccessAllowed: true + ) + + let completeFile = TempBox.shared.tempFile(fileName: "data") + let metaFile = TempBox.shared.tempFile(fileName: "data") + + guard let fileContext = MediaBoxFileContextV2Impl( + queue: queue, + manager: postbox.mediaBox.dataFileManager, + storageBox: nil, + resourceId: file.media.resource.id.stringRepresentation.data(using: .utf8)!, + path: completeFile.path, + partialPath: partialFile.path, + metaPath: metaFile.path + ) else { + return EmptyDisposable + } + + let fetchDisposable = fileContext.fetched( + range: mappedRange, + priority: .default, + fetch: { intervals in + return fetchResource(file.media.resource, intervals, params) + }, + error: { _ in + }, + completed: { + } + ) + + #if DEBUG + let startTime = CFAbsoluteTimeGetCurrent() + #endif + + let dataDisposable = fileContext.data( + range: mappedRange, + waitUntilAfterInitialFetch: true, + next: { result in + if result.complete { + #if DEBUG + let fetchTime = CFAbsoluteTimeGetCurrent() - startTime + print("Fetching \(quality)p part took \(fetchTime * 1000.0) ms") + #endif + subscriber.putNext((partialFile, Int(result.offset) ..< Int(result.offset + result.size), Int(size))) + subscriber.putCompletion() + } + } + ) + + return ActionDisposable { + queue.async { + fetchDisposable.dispose() + dataDisposable.dispose() + fileContext.cancelFullRangeFetches() + + TempBox.shared.dispose(completeFile) + TempBox.shared.dispose(metaFile) + } + } + } + |> runOn(queue) + + return fetchFromRemote + } +} + +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) + } +} + +private final class SharedHLSVideoWebView: NSObject, WKNavigationDelegate { + private final class ContextReference { + weak var contentNode: HLSVideoJSNativeContentNode? + + init(contentNode: HLSVideoJSNativeContentNode?) { + self.contentNode = contentNode + } + } + + private enum ResponseError { + case badRequest + case notFound + case internalServerError + + var httpStatus: (Int, String) { + switch self { + case .badRequest: + return (400, "Bad Request") + case .notFound: + return (404, "Not Found") + case .internalServerError: + return (500, "Internal Server Error") + } + } + } + + static let shared: SharedHLSVideoWebView = SharedHLSVideoWebView() + + private var contextReferences: [Int: ContextReference] = [:] + + var webView: WKWebView? + + var videoElements: [Int: VideoElement] = [:] + var mediaSources: [Int: MediaSource] = [:] + var sourceBuffers: [Int: SourceBuffer] = [:] + + private var isWebViewReady: Bool = false + private var pendingInitializeInstanceIds: [(id: Int, urlPrefix: String)] = [] + + private var tempTasks: [Int: URLSessionTask] = [:] + + private var emptyTimer: Foundation.Timer? + + override init() { + super.init() + } + + deinit { + self.emptyTimer?.invalidate() + } + + private func createWebView() { + let config = WKWebViewConfiguration() + config.allowsInlineMediaPlayback = true + config.mediaTypesRequiringUserActionForPlayback = [] + config.allowsPictureInPictureMediaPlayback = true + + 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 + + config.userContentController = userController + + let webView = WKWebView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0)), configuration: config) + self.webView = webView + + webView.scrollView.isScrollEnabled = false + webView.allowsLinkPreview = false + webView.allowsBackForwardNavigationGestures = false + webView.accessibilityIgnoresInvertColors = true + webView.scrollView.contentInsetAdjustmentBehavior = .never + webView.alpha = 0.0 + + if #available(iOS 16.4, *) { + webView.isInspectable = isDebug + } + + webView.navigationDelegate = self + + 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 "windowOnLoad": + self.isWebViewReady = true + + self.initializePendingInstances() + case "bridgeInvoke": + guard let eventData = body["data"] as? [String: Any] else { + return + } + guard let bridgeId = eventData["bridgeId"] as? Int else { + return + } + guard let callbackId = eventData["callbackId"] as? Int else { + return + } + guard let className = eventData["className"] as? String else { + return + } + guard let methodName = eventData["methodName"] as? String else { + return + } + guard let params = eventData["params"] as? [String: Any] else { + return + } + self.bridgeInvoke( + bridgeId: bridgeId, + className: className, + methodName: methodName, + params: params, + completion: { [weak self] result in + guard let self else { + return + } + let jsonResult = try! JSONSerialization.data(withJSONObject: result) + let jsonResultString = String(data: jsonResult, encoding: .utf8)! + self.webView?.evaluateJavaScript("bridgeInvokeCallback(\(callbackId), \(jsonResultString));", completionHandler: nil) + } + ) + case "playerStatus": + guard let instanceId = body["instanceId"] as? Int else { + return + } + guard let instance = self.contextReferences[instanceId]?.contentNode else { + self.contextReferences.removeValue(forKey: instanceId) + return + } + guard let eventData = body["data"] as? [String: Any] else { + return + } + + instance.onPlayerStatusUpdated(eventData: eventData) + case "playerCurrentTime": + guard let instanceId = body["instanceId"] as? Int else { + return + } + guard let instance = self.contextReferences[instanceId]?.contentNode else { + self.contextReferences.removeValue(forKey: instanceId) + return + } + guard let eventData = body["data"] as? [String: Any] else { + return + } + guard let value = eventData["value"] as? Double else { + return + } + + instance.onPlayerUpdatedCurrentTime(currentTime: value) + + var bandwidthEstimate = eventData["bandwidthEstimate"] as? Double + if let bandwidthEstimateValue = bandwidthEstimate, bandwidthEstimateValue.isNaN || bandwidthEstimateValue.isInfinite { + bandwidthEstimate = nil + } + + HLSVideoJSNativeContentNode.sharedBandwidthEstimate = bandwidthEstimate + default: + break + } + } + } + + self.isWebViewReady = false + + let bundle = Bundle(for: SharedHLSVideoWebView.self) + let bundlePath = bundle.bundlePath + "/HlsBundle.bundle" + webView.loadFileURL(URL(fileURLWithPath: bundlePath + "/index.html"), allowingReadAccessTo: URL(fileURLWithPath: bundlePath)) + } + + private func disposeWebView() { + if let _ = self.webView { + self.webView = nil + } + self.isWebViewReady = false + } + + private func bridgeInvoke( + bridgeId: Int, + className: String, + methodName: String, + params: [String: Any], + completion: @escaping ([String: Any]) -> Void + ) { + if (className == "VideoElement") { + if (methodName == "constructor") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + let videoElement = VideoElement(instanceId: instanceId) + SharedHLSVideoWebView.shared.videoElements[bridgeId] = videoElement + completion([:]) + } else if (methodName == "setMediaSource") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + guard let mediaSourceId = params["mediaSourceId"] as? Int else { + assertionFailure() + return + } + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == instanceId }) else { + return + } + videoElement.mediaSourceId = mediaSourceId + } else if (methodName == "setCurrentTime") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + guard let currentTime = params["currentTime"] as? Double else { + assertionFailure() + return + } + + if let instance = self.contextReferences[instanceId]?.contentNode { + instance.onSetCurrentTime(timestamp: currentTime) + } + + completion([:]) + } else if (methodName == "setPlaybackRate") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + guard let playbackRate = params["playbackRate"] as? Double else { + assertionFailure() + return + } + + if let instance = self.contextReferences[instanceId]?.contentNode { + instance.onSetPlaybackRate(playbackRate: playbackRate) + } + + completion([:]) + } else if (methodName == "play") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + + if let instance = self.contextReferences[instanceId]?.contentNode { + instance.onPlay() + } + + completion([:]) + } else if (methodName == "pause") { + guard let instanceId = params["instanceId"] as? Int else { + assertionFailure() + return + } + + if let instance = self.contextReferences[instanceId]?.contentNode { + instance.onPause() + } + + completion([:]) + } + } else if (className == "MediaSource") { + if (methodName == "constructor") { + let mediaSource = MediaSource() + SharedHLSVideoWebView.shared.mediaSources[bridgeId] = mediaSource + completion([:]) + } else if (methodName == "setDuration") { + guard let duration = params["duration"] as? Double else { + assertionFailure() + return + } + guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[bridgeId] else { + assertionFailure() + return + } + var durationUpdated = false + if mediaSource.duration != duration { + mediaSource.duration = duration + durationUpdated = true + } + + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.mediaSourceId == bridgeId }) else { + return + } + + if let instance = self.contextReferences[videoElement.instanceId]?.contentNode { + if durationUpdated { + instance.onMediaSourceDurationUpdated() + } + } + completion([:]) + } else if (methodName == "updateSourceBuffers") { + guard let ids = params["ids"] as? [Int] else { + assertionFailure() + return + } + guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[bridgeId] else { + assertionFailure() + return + } + mediaSource.sourceBufferIds = ids + + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.mediaSourceId == bridgeId }) else { + return + } + + if let instance = self.contextReferences[videoElement.instanceId]?.contentNode { + instance.onMediaSourceBuffersUpdated() + } + } + } else if (className == "SourceBuffer") { + if (methodName == "constructor") { + guard let mediaSourceId = params["mediaSourceId"] as? Int else { + assertionFailure() + return + } + guard let mimeType = params["mimeType"] as? String else { + assertionFailure() + return + } + let sourceBuffer = SourceBuffer(mediaSourceId: mediaSourceId, mimeType: mimeType) + SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] = sourceBuffer + + completion([:]) + } else if (methodName == "appendBuffer") { + guard let base64Data = params["data"] as? String else { + assertionFailure() + return + } + guard let data = Data(base64Encoded: base64Data.data(using: .utf8)!) else { + assertionFailure() + return + } + guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else { + assertionFailure() + return + } + sourceBuffer.appendBuffer(data: data, completion: { bufferedRanges in + completion(["ranges": serializeRanges(bufferedRanges)]) + }) + } else if methodName == "remove" { + guard let start = params["start"] as? Double, let end = params["end"] as? Double else { + assertionFailure() + return + } + guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else { + assertionFailure() + return + } + sourceBuffer.remove(start: start, end: end, completion: { bufferedRanges in + completion(["ranges": serializeRanges(bufferedRanges)]) + }) + } else if methodName == "abort" { + guard let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[bridgeId] else { + assertionFailure() + return + } + sourceBuffer.abortOperation() + completion([:]) + } + } else if className == "XMLHttpRequest" { + if methodName == "load" { + guard let id = params["id"] as? Int else { + assertionFailure() + return + } + guard let url = params["url"] as? String else { + assertionFailure() + return + } + guard let requestHeaders = params["requestHeaders"] as? [String: String] else { + assertionFailure() + return + } + guard let parsedUrl = URL(string: url) else { + assertionFailure() + return + } + guard let host = parsedUrl.host, host == "server" else { + completion(["error": 1]) + return + } + + var requestPath = parsedUrl.path + if requestPath.hasPrefix("/") { + requestPath = String(requestPath[requestPath.index(after: requestPath.startIndex) ..< requestPath.endIndex]) + } + + guard let firstSlash = requestPath.range(of: "/") else { + completion(["error": 1]) + return + } + + var requestRange: Range? + if let rangeString = requestHeaders["Range"] { + requestRange = parseRange(from: rangeString) + } + + let streamId = String(requestPath[requestPath.startIndex ..< firstSlash.lowerBound]) + + var handlerFound = false + for (_, contextReference) in self.contextReferences { + if let context = contextReference.contentNode, let source = context.playerSource, source.id == streamId { + handlerFound = true + + let filePath = String(requestPath[firstSlash.upperBound...]) + if filePath == "master.m3u8" { + let _ = (source.masterPlaylistData() + |> deliverOn(.mainQueue()) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + self.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion) + }) + } 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(id: id, error: .notFound, completion: completion) + return + } + + let _ = (source.playlistData(quality: levelIndex) + |> deliverOn(.mainQueue()) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + self.sendResponseAndClose(id: id, data: result.data(using: .utf8)!, completion: completion) + }) + } 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(id: id, error: .notFound, completion: completion) + return + } + guard let requestRange else { + self.sendErrorAndClose(id: id, error: .badRequest, completion: completion) + return + } + let _ = (source.fileData(id: fileIdValue, range: requestRange.lowerBound ..< requestRange.upperBound + 1) + |> deliverOn(.mainQueue()) + //|> timeout(5.0, queue: self.queue, alternate: .single(nil)) + |> take(1)).start(next: { [weak self] result in + guard let self else { + return + } + + if let (tempFile, tempFileRange, totalSize) = result { + self.sendResponseFileAndClose(id: id, file: tempFile, fileRange: tempFileRange, range: requestRange, totalSize: totalSize, completion: completion) + } else { + self.sendErrorAndClose(id: id, error: .internalServerError, completion: completion) + } + }) + } + + break + } + } + + if (!handlerFound) { + completion(["error": 1]) + } + + /*var request = URLRequest(url: URL(string: url)!) + for (key, value) in requestHeaders { + request.setValue(value, forHTTPHeaderField: key) + } + + let isCompleted = Atomic(value: false) + let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] data, response, error in + Queue.mainQueue().async { + guard let self else { + return + } + if isCompleted.swap(true) { + return + } + + self.tempTasks.removeValue(forKey: id) + + if let _ = error { + completion([ + "error": 1 + ]) + } else { + if let response = response as? HTTPURLResponse { + completion([ + "status": response.statusCode, + "statusText": "OK", + "responseData": data?.base64EncodedString() ?? "", + "responseHeaders": response.allHeaderFields as? [String: String] ?? [:] + ]) + + let _ = response + /*if let response = response as? HTTPURLResponse, let requestUrl { + if let updatedResponse = HTTPURLResponse( + url: requestUrl, + statusCode: response.statusCode, + httpVersion: "HTTP/1.1", + headerFields: response.allHeaderFields as? [String: String] ?? [:] + ) { + sourceTask.didReceive(updatedResponse) + } else { + sourceTask.didReceive(response) + } + } else { + sourceTask.didReceive(response) + }*/ + } + } + } + }) + self.tempTasks[id] = task + task.resume()*/ + } else if methodName == "abort" { + guard let id = params["id"] as? Int else { + assertionFailure() + return + } + + if let task = self.tempTasks.removeValue(forKey: id) { + task.cancel() + } + + completion([:]) + } + } + } + + private func sendErrorAndClose(id: Int, error: ResponseError, completion: @escaping ([String: Any]) -> Void) { + let (code, status) = error.httpStatus + completion([ + "status": code, + "statusText": status, + "responseData": "", + "responseHeaders": [ + "Content-Type": "text/html" + ] as [String: String] + ]) + } + + private func sendResponseAndClose(id: Int, data: Data, contentType: String = "application/octet-stream", completion: @escaping ([String: Any]) -> Void) { + completion([ + "status": 200, + "statusText": "OK", + "responseData": data.base64EncodedString(), + "responseHeaders": [ + "Content-Type": contentType, + "Content-Length": "\(data.count)" + ] as [String: String] + ]) + } + + private func sendResponseFileAndClose(id: Int, file: TempBoxFile, fileRange: Range, range: Range, totalSize: Int, completion: @escaping ([String: Any]) -> Void) { + if let data = try? Data(contentsOf: URL(fileURLWithPath: file.path), options: .mappedIfSafe).subdata(in: fileRange) { + completion([ + "status": 200, + "statusText": "OK", + "responseData": data.base64EncodedString(), + "responseHeaders": [ + "Content-Type": "application/octet-stream", + "Content-Range": "bytes \(range.lowerBound)-\(range.upperBound)/\(totalSize)", + "Content-Length": "\(fileRange.upperBound - fileRange.lowerBound)" + ] as [String: String] + ]) + } else { + self.sendErrorAndClose(id: id, error: .internalServerError, completion: completion) + } + } + + func register(context: HLSVideoJSNativeContentNode) -> Disposable { + let contextInstanceId = context.instanceId + self.contextReferences[contextInstanceId] = ContextReference(contentNode: context) + + if self.webView == nil { + self.createWebView() + } + + if let emptyTimer = self.emptyTimer { + self.emptyTimer = nil + emptyTimer.invalidate() + } + + return ActionDisposable { [weak self, weak context] in + Queue.mainQueue().async { + guard let self else { + return + } + self.pendingInitializeInstanceIds.removeAll(where: { $0.id == contextInstanceId }) + + if let current = self.contextReferences[contextInstanceId] { + if let value = current.contentNode { + if let context, context === value { + self.contextReferences.removeValue(forKey: contextInstanceId) + } + } else { + self.contextReferences.removeValue(forKey: contextInstanceId) + } + } + + self.webView?.evaluateJavaScript("window.hlsPlayer_destroyInstance(\(contextInstanceId));") + + if self.contextReferences.isEmpty { + if self.emptyTimer == nil { + self.emptyTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false, block: { [weak self] timer in + guard let self else { + return + } + if self.emptyTimer === timer { + self.emptyTimer = nil + } + if self.contextReferences.isEmpty { + self.disposeWebView() + } + }) + } + } + } + } + } + + func initializeWhenReady(context: HLSVideoJSNativeContentNode, urlPrefix: String) { + self.pendingInitializeInstanceIds.append((context.instanceId, urlPrefix)) + + if self.isWebViewReady { + self.initializePendingInstances() + } + } + + private func initializePendingInstances() { + let pendingInitializeInstanceIds = self.pendingInitializeInstanceIds + self.pendingInitializeInstanceIds.removeAll() + + if pendingInitializeInstanceIds.isEmpty { + return + } + + let isDebug: Bool + #if DEBUG + isDebug = true + #else + isDebug = false + #endif + + var userScriptJs = "" + for (instanceId, urlPrefix) in pendingInitializeInstanceIds { + guard let _ = self.contextReferences[instanceId]?.contentNode else { + self.contextReferences.removeValue(forKey: instanceId) + continue + } + userScriptJs.append("window.hlsPlayer_makeInstance(\(instanceId));\n") + userScriptJs.append(""" + window.hlsPlayer_instances[\(instanceId)].playerInitialize({ + 'debug': \(isDebug), + 'bandwidthEstimate': \(HLSVideoJSNativeContentNode.sharedBandwidthEstimate ?? 500000.0), + 'urlPrefix': '\(urlPrefix)' + });\n + """) + } + + self.webView?.evaluateJavaScript(userScriptJs) + } +} + +final class HLSVideoJSNativeContentNode: ASDisplayNode, UniversalVideoContentNode { + fileprivate 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 struct VideoQualityState: Equatable { + var current: Int + var preferred: UniversalVideoContentVideoQuality + var available: [Int] + + init(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int]) { + self.current = current + self.preferred = preferred + self.available = available + } + } + + fileprivate 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 + + fileprivate let playerSource: HLSJSServerSource? + 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() + var status: Signal { + return self._status.get() + } + + private let _bufferingStatus = Promise<(RangeSet, Int64)?>() + var bufferingStatus: Signal<(RangeSet, Int64)?, NoError> { + return self._bufferingStatus.get() + } + + var isNativePictureInPictureActive: Signal { + return .single(false) + } + + private let _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let _preloadCompleted = ValuePromise() + var preloadCompleted: Signal { + return self._preloadCompleted.get() + } + + private static var nextInstanceId: Int = 0 + fileprivate let instanceId: Int + + private let imageNode: TransformImageNode + + private let player: ChunkMediaPlayer + private let playerNode: MediaPlayerNode + + private let fetchDisposable = MetaDisposable() + + private var dimensions: CGSize? + private let dimensionsPromise = ValuePromise(CGSize()) + + private var validLayout: (size: CGSize, actualSize: CGSize)? + + private var statusTimer: Foundation.Timer? + + private var preferredVideoQuality: UniversalVideoContentVideoQuality = .auto + + fileprivate var playerIsReady: Bool = false + fileprivate var playerIsPlaying: Bool = false + fileprivate var playerRate: Double = 0.0 + fileprivate var playerDefaultRate: Double = 1.0 + fileprivate var playerTime: Double = 0.0 + + fileprivate var playerAvailableLevels: [Int: Level] = [:] + fileprivate var playerCurrentLevelIndex: Int? + + private var videoQualityStateValue: VideoQualityState? + private let videoQualityStatePromise = Promise(nil) + + private var hasRequestedPlayerLoad: Bool = false + + private var requestedBaseRate: Double = 1.0 + private var requestedLevelIndex: Int? + + private var didBecomeActiveObserver: NSObjectProtocol? + private var willResignActiveObserver: NSObjectProtocol? + + private let chunkPlayerPartsState = Promise(ChunkMediaPlayerPartsState(duration: nil, parts: [])) + private var sourceBufferStateDisposable: Disposable? + + private var playerStatusDisposable: Disposable? + + private var contextDisposable: Disposable? + + init(accountId: AccountRecordId, postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, streamVideo: Bool, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool) { + self.instanceId = HLSVideoJSNativeContentNode.nextInstanceId + HLSVideoJSNativeContentNode.nextInstanceId += 1 + + self.postbox = postbox + self.fileReference = fileReference + self.approximateDuration = fileReference.media.duration ?? 0.0 + self.audioSessionManager = audioSessionManager + self.userLocation = userLocation + self.requestedBaseRate = 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 playerSource: HLSJSServerSource? + if let qualitySet = HLSQualitySet(baseFile: fileReference) { + let playerSourceValue = HLSJSServerSource(accountId: accountId.int64, fileId: fileReference.media.fileId.id, postbox: postbox, userLocation: userLocation, playlistFiles: qualitySet.playlistFiles, qualityFiles: qualitySet.qualityFiles) + playerSource = playerSourceValue + } + self.playerSource = playerSource + + let mediaDimensions = fileReference.media.dimensions?.cgSize ?? CGSize(width: 480.0, height: 320.0) + var intrinsicDimensions = mediaDimensions.aspectFittedOrSmaller(CGSize(width: 1280.0, height: 1280.0)) + + intrinsicDimensions.width = floor(intrinsicDimensions.width / UIScreenScale) + intrinsicDimensions.height = floor(intrinsicDimensions.height / UIScreenScale) + self.intrinsicDimensions = intrinsicDimensions + + var onSeeked: (() -> Void)? + self.player = ChunkMediaPlayer( + postbox: postbox, + audioSessionManager: audioSessionManager, + partsState: self.chunkPlayerPartsState.get(), + video: true, + enableSound: true, + baseRate: baseRate, + onSeeked: { + onSeeked?() + } + ) + + self.playerNode = MediaPlayerNode() + self.player.attachPlayerNode(self.playerNode) + + super.init() + + self.contextDisposable = SharedHLSVideoWebView.shared.register(context: self) + + self.playerNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicDimensions) + + let thumbnailVideoReference = HLSVideoContent.minimizedHLSQuality(file: fileReference)?.file ?? fileReference + + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: self.userLocation, videoReference: thumbnailVideoReference, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true) |> 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.imageNode.imageUpdated = { [weak self] _ in + self?._ready.set(.single(Void())) + } + + self._bufferingStatus.set(.single(nil)) + + self.didBecomeActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in + let _ = self + }) + self.willResignActiveObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in + let _ = self + }) + + self.playerStatusDisposable = (self.player.status + |> deliverOnMainQueue).startStrict(next: { [weak self] status in + guard let self else { + return + } + self.updatePlayerStatus(status: status) + }) + + self.statusTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 25.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.updateStatus() + }) + + onSeeked = { [weak self] in + Queue.mainQueue().async { + guard let self else { + return + } + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerNotifySeekedOnNextStatusUpdate();", completionHandler: nil) + } + } + + if let playerSource { + SharedHLSVideoWebView.shared.initializeWhenReady(context: self, urlPrefix: "http://server/\(playerSource.id)/") + } + } + + deinit { + if let didBecomeActiveObserver = self.didBecomeActiveObserver { + NotificationCenter.default.removeObserver(didBecomeActiveObserver) + } + if let willResignActiveObserver = self.willResignActiveObserver { + NotificationCenter.default.removeObserver(willResignActiveObserver) + } + + self.serverDisposable?.dispose() + self.audioSessionDisposable.dispose() + + self.statusTimer?.invalidate() + + self.sourceBufferStateDisposable?.dispose() + self.playerStatusDisposable?.dispose() + + self.contextDisposable?.dispose() + } + + fileprivate func onPlayerStatusUpdated(eventData: [String: Any]) { + if let isReady = eventData["isReady"] as? Bool { + self.playerIsReady = isReady + } else { + self.playerIsReady = 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] = HLSVideoJSNativeContentNode.Level( + bitrate: levelBitrate, + width: levelWidth, + height: levelHeight + ) + } + } else { + self.playerAvailableLevels.removeAll() + } + + if let currentLevel = eventData["currentLevel"] as? Int { + if self.playerAvailableLevels[currentLevel] != nil { + self.playerCurrentLevelIndex = currentLevel + } else { + self.playerCurrentLevelIndex = nil + } + } else { + self.playerCurrentLevelIndex = nil + } + + self.updateVideoQualityState() + + 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 + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerLoad(\(selectedLevelIndex));", completionHandler: nil) + } + } + } + + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetBaseRate(\(self.requestedBaseRate));", completionHandler: nil) + } + + self.updateStatus() + } + + fileprivate func onPlayerUpdatedCurrentTime(currentTime: Double) { + self.playerTime = currentTime + + self.updateStatus() + } + + fileprivate func onSetCurrentTime(timestamp: Double) { + self.player.seek(timestamp: timestamp) + } + + fileprivate func onSetPlaybackRate(playbackRate: Double) { + self.player.setBaseRate(playbackRate) + } + + fileprivate func onPlay() { + self.player.play() + } + + fileprivate func onPause() { + self.player.pause() + } + + fileprivate func onMediaSourceDurationUpdated() { + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else { + return + } + guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else { + return + } + guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else { + return + } + + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + } + + fileprivate func onMediaSourceBuffersUpdated() { + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else { + return + } + guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else { + return + } + guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else { + return + } + + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + if self.sourceBufferStateDisposable == nil { + self.sourceBufferStateDisposable = (sourceBuffer.updated.signal() + |> deliverOnMainQueue).startStrict(next: { [weak self, weak sourceBuffer] _ in + guard let self, let sourceBuffer else { + return + } + guard let mediaSource = SharedHLSVideoWebView.shared.mediaSources[sourceBuffer.mediaSourceId] else { + return + } + self.chunkPlayerPartsState.set(.single(ChunkMediaPlayerPartsState(duration: mediaSource.duration, parts: sourceBuffer.items))) + + self.updateBuffered() + }) + } + } + + private func updatePlayerStatus(status: MediaPlayerStatus) { + self._status.set(status) + + if let (bridgeId, _) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) { + var isPlaying: Bool = false + var isBuffering = false + switch status.status { + case .playing: + isPlaying = true + case .paused: + break + case let .buffering(_, whilePlaying, _, _): + isPlaying = whilePlaying + isBuffering = true + } + + let result: [String: Any] = [ + "isPlaying": isPlaying, + "isWaiting": isBuffering, + "currentTime": status.timestamp + ] + + let jsonResult = try! JSONSerialization.data(withJSONObject: result) + let jsonResultString = String(data: jsonResult, encoding: .utf8)! + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.bridgeObjectMap[\(bridgeId)].bridgeUpdateStatus(\(jsonResultString));", completionHandler: nil) + } + } + + private func updateBuffered() { + guard let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) else { + return + } + guard let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] else { + return + } + guard let sourceBufferId = mediaSource.sourceBufferIds.first, let sourceBuffer = SharedHLSVideoWebView.shared.sourceBuffers[sourceBufferId] else { + return + } + + let bufferedRanges = sourceBuffer.ranges + + if let (_, videoElement) = SharedHLSVideoWebView.shared.videoElements.first(where: { $0.value.instanceId == self.instanceId }) { + if let mediaSourceId = videoElement.mediaSourceId, let mediaSource = SharedHLSVideoWebView.shared.mediaSources[mediaSourceId] { + if let duration = mediaSource.duration { + var mappedRanges = RangeSet() + for range in bufferedRanges.ranges { + let rangeLower = max(0.0, range.lowerBound - 0.2) + let rangeUpper = min(duration, range.upperBound + 0.2) + mappedRanges.formUnion(RangeSet(Int64(rangeLower * 1000.0) ..< Int64(rangeUpper * 1000.0))) + } + self._bufferingStatus.set(.single((mappedRanges, Int64(duration * 1000.0)))) + } + } + } + } + + private func updateStatus() { + } + + 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.requestedBaseRate, seekId: self.seekId, status: .buffering(initial: true, whilePlaying: true, progress: 0.0, display: true), soundEnabled: true)) + } + self.player.play() + } + + func pause() { + assert(Queue.mainQueue().isCurrent()) + self.player.pause() + } + + func togglePlayPause() { + assert(Queue.mainQueue().isCurrent()) + self.player.togglePlayPause() + } + + func setSoundEnabled(_ value: Bool) { + assert(Queue.mainQueue().isCurrent()) + if value { + self.player.playOnceWithSound(playAndRecord: false, seek: .none) + } else { + self.player.continuePlayingWithoutSound(seek: .none) + } + } + + func seek(_ timestamp: Double) { + assert(Queue.mainQueue().isCurrent()) + self.seekId += 1 + + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSeek(\(timestamp));", completionHandler: nil) + } + + func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + assert(Queue.mainQueue().isCurrent()) + let action = { [weak self] in + Queue.mainQueue().async { + self?.performActionAtEnd() + } + } + switch actionAtEnd { + case .loop: + self.player.actionAtEnd = .loop({}) + case .loopDisablingSound: + self.player.actionAtEnd = .loopDisablingSound(action) + case .stop: + self.player.actionAtEnd = .action(action) + case .repeatIfNeeded: + let _ = (self.player.status + |> deliverOnMainQueue + |> take(1)).start(next: { [weak self] status in + guard let strongSelf = self else { + return + } + if status.timestamp > status.duration * 0.1 { + strongSelf.player.actionAtEnd = .loop({ [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.player.actionAtEnd = .loopDisablingSound(action) + }) + } else { + strongSelf.player.actionAtEnd = .loopDisablingSound(action) + } + }) + } + + self.player.playOnceWithSound(playAndRecord: playAndRecord, seek: seek) + } + + func setSoundMuted(soundMuted: Bool) { + self.player.setSoundMuted(soundMuted: soundMuted) + } + + func continueWithOverridingAmbientMode(isAmbient: Bool) { + self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient) + } + + func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) { + assert(Queue.mainQueue().isCurrent()) + self.player.setForceAudioToSpeaker(forceAudioToSpeaker) + } + + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { + assert(Queue.mainQueue().isCurrent()) + let action = { [weak self] in + Queue.mainQueue().async { + self?.performActionAtEnd() + } + } + switch actionAtEnd { + case .loop: + self.player.actionAtEnd = .loop({}) + case .loopDisablingSound, .repeatIfNeeded: + self.player.actionAtEnd = .loopDisablingSound(action) + case .stop: + self.player.actionAtEnd = .action(action) + } + self.player.continuePlayingWithoutSound(seek: .none) + } + + func setContinuePlayingWithoutSoundOnLostAudioSession(_ value: Bool) { + self.player.setContinuePlayingWithoutSoundOnLostAudioSession(value) + } + + func setBaseRate(_ baseRate: Double) { + self.requestedBaseRate = baseRate + if self.playerIsReady { + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].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: { min($0.value.width, $0.value.height) == quality }) { + self.requestedLevelIndex = level.key + } else { + self.requestedLevelIndex = nil + } + } + + self.updateVideoQualityState() + + if self.playerIsReady { + SharedHLSVideoWebView.shared.webView?.evaluateJavaScript("window.hlsPlayer_instances[\(self.instanceId)].playerSetLevel(\(self.requestedLevelIndex ?? -1));", completionHandler: nil) + } + } + + private func updateVideoQualityState() { + var videoQualityState: VideoQualityState? + if let value = self.videoQualityState() { + videoQualityState = VideoQualityState(current: value.current, preferred: value.preferred, available: value.available) + } + if self.videoQualityStateValue != videoQualityState { + self.videoQualityStateValue = videoQualityState + self.videoQualityStatePromise.set(.single(videoQualityState)) + } + } + + func videoQualityState() -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? { + if self.playerAvailableLevels.isEmpty { + if let qualitySet = HLSQualitySet(baseFile: self.fileReference), let minQualityFile = HLSVideoContent.minimizedHLSQuality(file: self.fileReference)?.file { + let sortedFiles = qualitySet.qualityFiles.sorted(by: { $0.key > $1.key }) + if let minQuality = sortedFiles.first(where: { $0.value.media.fileId == minQualityFile.media.fileId }) { + return (minQuality.key, .auto, sortedFiles.map(\.key)) + } + } + } + + guard let playerCurrentLevelIndex = self.playerCurrentLevelIndex else { + return nil + } + guard let currentLevel = self.playerAvailableLevels[playerCurrentLevelIndex] else { + return nil + } + + var available = self.playerAvailableLevels.values.map { min($0.width, $0.height) } + available.sort(by: { $0 > $1 }) + + return (min(currentLevel.width, currentLevel.height), self.preferredVideoQuality, available) + } + + public func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return self.videoQualityStatePromise.get() + |> map { value -> (current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])? in + guard let value else { + return nil + } + return (value.current, value.preferred, value.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) { + self.playerNode.setCanPlaybackWithoutHierarchy(canPlaybackWithoutHierarchy) + } + + func enterNativePictureInPicture() -> Bool { + return false + } + + func exitNativePictureInPicture() { + } + + func setNativePictureInPictureIsActive(_ value: Bool) { + self.imageNode.isHidden = !value + } +} + +private func serializeRanges(_ ranges: RangeSet) -> [Double] { + var result: [Double] = [] + for range in ranges.ranges { + result.append(range.lowerBound) + result.append(range.upperBound) + } + return result +} + +private final class VideoElement { + let instanceId: Int + + var mediaSourceId: Int? + + init(instanceId: Int) { + self.instanceId = instanceId + } +} + +private final class MediaSource { + var duration: Double? + var sourceBufferIds: [Int] = [] + + init() { + } +} + +private final class SourceBuffer { + private static let sharedQueue = Queue(name: "SourceBuffer") + + final class Item { + let tempFile: TempBoxFile + let asset: AVURLAsset + let startTime: Double + let endTime: Double + let rawData: Data + + var clippedStartTime: Double + var clippedEndTime: Double + + init(tempFile: TempBoxFile, asset: AVURLAsset, startTime: Double, endTime: Double, rawData: Data) { + self.tempFile = tempFile + self.asset = asset + self.startTime = startTime + self.endTime = endTime + self.rawData = rawData + + self.clippedStartTime = startTime + self.clippedEndTime = endTime + } + + func removeRange(start: Double, end: Double) { + //TODO + } + } + + let mediaSourceId: Int + let mimeType: String + var initializationData: Data? + var items: [ChunkMediaPlayerPart] = [] + var ranges = RangeSet() + + let updated = ValuePipe() + + private var currentUpdateId: Int = 0 + + init(mediaSourceId: Int, mimeType: String) { + self.mediaSourceId = mediaSourceId + self.mimeType = mimeType + } + + func abortOperation() { + self.currentUpdateId += 1 + } + + func appendBuffer(data: Data, completion: @escaping (RangeSet) -> Void) { + let initializationData = self.initializationData + self.currentUpdateId += 1 + let updateId = self.currentUpdateId + + SourceBuffer.sharedQueue.async { [weak self] in + let tempFile = TempBox.shared.tempFile(fileName: "data.mp4") + + var combinedData = Data() + if let initializationData { + combinedData.append(initializationData) + } + combinedData.append(data) + guard let _ = try? combinedData.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) else { + Queue.mainQueue().async { + guard let self else { + completion(RangeSet()) + return + } + + if self.currentUpdateId != updateId { + return + } + + completion(self.ranges) + } + return + } + + if let fragmentInfo = extractFFMpegMediaInfo(path: tempFile.path) { + Queue.mainQueue().async { + guard let self else { + completion(RangeSet()) + return + } + + if self.currentUpdateId != updateId { + return + } + + if fragmentInfo.duration.value == 0 { + self.initializationData = data + + completion(self.ranges) + } else { + let item = ChunkMediaPlayerPart( + startTime: fragmentInfo.startTime.seconds, + endTime: fragmentInfo.startTime.seconds + fragmentInfo.duration.seconds, + file: tempFile + ) + self.items.append(item) + self.updateRanges() + + completion(self.ranges) + + self.updated.putNext(Void()) + } + } + } else { + assertionFailure() + Queue.mainQueue().async { + guard let self else { + completion(RangeSet()) + return + } + + if self.currentUpdateId != updateId { + return + } + + completion(self.ranges) + } + return + } + } + } + + func remove(start: Double, end: Double, completion: @escaping (RangeSet) -> Void) { + self.items.removeAll(where: { item in + if item.startTime >= start && item.endTime <= end { + return true + } else { + return false + } + }) + self.updateRanges() + completion(self.ranges) + + self.updated.putNext(Void()) + } + + private func updateRanges() { + self.ranges = RangeSet() + for item in self.items { + let itemStartTime = round(item.startTime * 1000.0) / 1000.0 + let itemEndTime = round(item.endTime * 1000.0) / 1000.0 + self.ranges.formUnion(RangeSet(itemStartTime ..< itemEndTime)) + } + } +} + +private func parseFragment(filePath: String) -> (offset: CMTime, duration: CMTime)? { + let source = SoftwareVideoSource(path: filePath, hintVP9: false, unpremultiplyAlpha: false) + return source.readTrackInfo() +} diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index f8e38da318..7a157b4635 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -36,6 +36,8 @@ public final class NativeVideoContent: UniversalVideoContent { public let nativeId: NativeVideoContentId public let userLocation: MediaResourceUserLocation public let fileReference: FileMediaReference + public let previewSourceFileReference: FileMediaReference? + public let limitedFileRange: Range? let imageReference: ImageMediaReference? public let dimensions: CGSize public let duration: Double @@ -68,11 +70,11 @@ public final class NativeVideoContent: UniversalVideoContent { return true } - if videoCodec == "av1" { + /*if videoCodec == "av1" { if isAv1Supported { return true } - } + }*/ return false } @@ -108,11 +110,13 @@ public final class NativeVideoContent: UniversalVideoContent { return file } - public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, baseVideoQuality: UniversalVideoContentVideoQuality = .auto, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) { + public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference? = nil, limitedFileRange: Range? = nil, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, baseVideoQuality: UniversalVideoContentVideoQuality = .auto, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) { self.id = id self.nativeId = id self.userLocation = userLocation self.fileReference = fileReference + self.previewSourceFileReference = previewSourceFileReference + self.limitedFileRange = limitedFileRange self.imageReference = imageReference if var dimensions = fileReference.media.dimensions { if let thumbnail = fileReference.media.previewRepresentations.first { @@ -154,7 +158,7 @@ public final class NativeVideoContent: UniversalVideoContent { } public func makeContentNode(accountId: AccountRecordId, postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, baseVideoQuality: self.baseVideoQuality, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, previewSourceFileReference: self.previewSourceFileReference, limitedFileRange: self.limitedFileRange, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, baseVideoQuality: self.baseVideoQuality, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -175,6 +179,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let postbox: Postbox private let userLocation: MediaResourceUserLocation private let fileReference: FileMediaReference + private let previewSourceFileReference: FileMediaReference? + private let limitedFileRange: Range? private let streamVideo: MediaPlayerStreaming private let enableSound: Bool private let soundMuted: Bool @@ -243,10 +249,12 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let hasSentFramesToDisplay: (() -> Void)? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, baseVideoQuality: UniversalVideoContentVideoQuality, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, previewSourceFileReference: FileMediaReference?, limitedFileRange: Range?, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, baseVideoQuality: UniversalVideoContentVideoQuality, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { self.postbox = postbox self.userLocation = userLocation self.fileReference = fileReference + self.previewSourceFileReference = previewSourceFileReference + self.limitedFileRange = limitedFileRange self.streamVideo = streamVideo self.placeholderColor = placeholderColor self.enableSound = enableSound @@ -275,7 +283,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent let selectedFile = NativeVideoContent.selectVideoQualityFile(file: fileReference.media, quality: self.baseVideoQuality) - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: fileReference.resourceReference(selectedFile.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, soundMuted: soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) + self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: fileReference.resourceReference(selectedFile.resource), tempFilePath: tempFilePath, limitedFileRange: limitedFileRange, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, soundMuted: soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) var actionAtEndImpl: (() -> Void)? if enableSound && !loopVideo { @@ -320,7 +328,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent setLayerDisableScreenshots(self.imageNode.layer, captureProtected) } - self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in + self.imageNode.setSignal(internalMediaGridMessageVideo(postbox: postbox, userLocation: userLocation, videoReference: fileReference, previewSourceFileReference: previewSourceFileReference, imageReference: imageReference, onlyFullSize: onlyFullSizeThumbnail, useLargeThumbnail: useLargeThumbnail, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail || fileReference.media.isInstantVideo) |> map { [weak self] getSize, getData in Queue.mainQueue().async { if let strongSelf = self, strongSelf.dimensions == nil { if let dimensions = getSize() { @@ -596,7 +604,6 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self._status.set(.never()) self.player.pause() - //TODO:release coordinate fetchAutomatically self.player = MediaPlayer(audioSessionManager: self.audioSessionManager, postbox: self.postbox, userLocation: self.userLocation, userContentType: userContentType, resourceReference: updatedFileReference.resourceReference(selectedFile.resource), tempFilePath: nil, streamable: self.streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: true, soundMuted: self.soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: nil, isAudioVideoMessage: self.isAudioVideoMessage) var actionAtEndImpl: (() -> Void)? @@ -660,6 +667,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent return nil } + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return .single(nil) + } + func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { assert(Queue.mainQueue().isCurrent()) let action = { [weak self] in @@ -712,4 +723,8 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent func exitNativePictureInPicture() { } + + func setNativePictureInPictureIsActive(_ value: Bool) { + self.imageNode.isHidden = value + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index a7e26bb004..2481eb2a85 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -459,6 +459,10 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte return nil } + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return .single(nil) + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } @@ -482,4 +486,7 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte func exitNativePictureInPicture() { } + + func setNativePictureInPictureIsActive(_ value: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index 46d2e9ed8e..fae099ae4c 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -296,6 +296,10 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent return nil } + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return .single(nil) + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } @@ -319,5 +323,8 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func exitNativePictureInPicture() { } + + func setNativePictureInPictureIsActive(_ value: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift index 294aa1211c..781ebfeb9c 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedPlayerNode.swift @@ -234,4 +234,7 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate { func exitNativePictureInPicture() { } + + func setNativePictureInPictureIsActive(_ value: Bool) { + } } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index 71c3c44d87..ebe1c8b9ac 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -194,6 +194,10 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { return nil } + func videoQualityStateSignal() -> Signal<(current: Int, preferred: UniversalVideoContentVideoQuality, available: [Int])?, NoError> { + return .single(nil) + } + func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int { return self.playbackCompletedListeners.add(f) } @@ -218,4 +222,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { func exitNativePictureInPicture() { } + + func setNativePictureInPictureIsActive(_ value: Bool) { + } } diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 093f02443c..b2537ee679 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -209,11 +209,21 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti } } if !skipEntity { + var hashtagValue = hashtag + var peerNameValue: String? + if hashtagValue.contains("@") { + let components = hashtagValue.components(separatedBy: "@") + if components.count == 2, let firstComponent = components.first, let lastComponent = components.last, !firstComponent.isEmpty && !lastComponent.isEmpty { + hashtagValue = firstComponent + peerNameValue = lastComponent + } + } + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if underlineLinks && underlineAllLinks { string.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: range) } - string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) + string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag), value: TelegramHashtag(peerName: peerNameValue, hashtag: hashtagValue), range: range) } case .BotCommand: string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index 1d0d9e7020..35f2fdfd62 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -9,23 +9,27 @@ public struct ChatTranslationState: Codable { enum CodingKeys: String, CodingKey { case baseLang case fromLang + case timestamp case toLang case isEnabled } public let baseLang: String public let fromLang: String + public let timestamp: Int32? public let toLang: String? public let isEnabled: Bool public init( baseLang: String, fromLang: String, + timestamp: Int32?, toLang: String?, isEnabled: Bool ) { self.baseLang = baseLang self.fromLang = fromLang + self.timestamp = timestamp self.toLang = toLang self.isEnabled = isEnabled } @@ -35,6 +39,7 @@ public struct ChatTranslationState: Codable { self.baseLang = try container.decode(String.self, forKey: .baseLang) self.fromLang = try container.decode(String.self, forKey: .fromLang) + self.timestamp = try container.decodeIfPresent(Int32.self, forKey: .timestamp) self.toLang = try container.decodeIfPresent(String.self, forKey: .toLang) self.isEnabled = try container.decode(Bool.self, forKey: .isEnabled) } @@ -44,6 +49,7 @@ public struct ChatTranslationState: Codable { try container.encode(self.baseLang, forKey: .baseLang) try container.encode(self.fromLang, forKey: .fromLang) + try container.encodeIfPresent(self.timestamp, forKey: .timestamp) try container.encodeIfPresent(self.toLang, forKey: .toLang) try container.encode(self.isEnabled, forKey: .isEnabled) } @@ -52,6 +58,7 @@ public struct ChatTranslationState: Codable { return ChatTranslationState( baseLang: self.baseLang, fromLang: self.fromLang, + timestamp: self.timestamp, toLang: toLang, isEnabled: self.isEnabled ) @@ -61,6 +68,7 @@ public struct ChatTranslationState: Codable { return ChatTranslationState( baseLang: self.baseLang, fromLang: self.fromLang, + timestamp: self.timestamp, toLang: self.toLang, isEnabled: isEnabled ) @@ -191,7 +199,8 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) return cachedChatTranslationState(engine: context.engine, peerId: peerId) |> mapToSignal { cached in - if let cached, cached.baseLang == baseLang { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let cached, let timestamp = cached.timestamp, cached.baseLang == baseLang && currentTime - timestamp < 60 * 60 { if !dontTranslateLanguages.contains(cached.fromLang) { return .single(cached) } else { @@ -246,20 +255,10 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) languageRecognizer.processString(text) let hypotheses = languageRecognizer.languageHypotheses(withMaximum: 4) languageRecognizer.reset() - - func normalize(_ code: String) -> String { - if code.contains("-") { - return code.components(separatedBy: "-").first ?? code - } else if code == "nb" { - return "no" - } else { - return code - } - } - - let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains(normalize($0.key.rawValue)) }.sorted(by: { $0.value > $1.value }) + + let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains(normalizeTranslationLanguage($0.key.rawValue)) }.sorted(by: { $0.value > $1.value }) if let language = filteredLanguages.first { - let fromLang = normalize(language.key.rawValue) + let fromLang = normalizeTranslationLanguage(language.key.rawValue) if loggingEnabled && !["en", "ru"].contains(fromLang) && !dontTranslateLanguages.contains(fromLang) { Logger.shared.log("ChatTranslation", "\(text)") Logger.shared.log("ChatTranslation", "Recognized as: \(fromLang), other hypotheses: \(hypotheses.map { $0.key.rawValue }.joined(separator: ",")) ") @@ -287,7 +286,13 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) if loggingEnabled { Logger.shared.log("ChatTranslation", "Ended with: \(fromLang)") } - let state = ChatTranslationState(baseLang: baseLang, fromLang: fromLang, toLang: nil, isEnabled: false) + let state = ChatTranslationState( + baseLang: baseLang, + fromLang: fromLang, + timestamp: currentTime, + toLang: cached?.toLang, + isEnabled: cached?.isEnabled ?? false + ) let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start() if !dontTranslateLanguages.contains(fromLang) { return state diff --git a/submodules/TranslateUI/Sources/Translate.swift b/submodules/TranslateUI/Sources/Translate.swift index 385858f398..64e664327b 100644 --- a/submodules/TranslateUI/Sources/Translate.swift +++ b/submodules/TranslateUI/Sources/Translate.swift @@ -157,6 +157,17 @@ public func effectiveIgnoredTranslationLanguages(context: AccountContext, ignore return dontTranslateLanguages } +public func normalizeTranslationLanguage(_ code: String) -> String { + var code = code + if code.contains("-") { + code = code.components(separatedBy: "-").first ?? code + } + if code == "nb" { + code = "no" + } + return code +} + public func canTranslateText(context: AccountContext, text: String, showTranslate: Bool, showTranslateIfTopical: Bool = false, ignoredLanguages: [String]?) -> (canTranslate: Bool, language: String?) { guard showTranslate || showTranslateIfTopical, text.count > 0 else { return (false, nil) @@ -178,20 +189,10 @@ public func canTranslateText(context: AccountContext, text: String, showTranslat if !showTranslate && showTranslateIfTopical { supportedTranslationLanguages = ["uk", "ru"] } - - func normalize(_ code: String) -> String { - if code.contains("-") { - return code.components(separatedBy: "-").first ?? code - } else if code == "nb" { - return "no" - } else { - return code - } - } - - let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains(normalize($0.key.rawValue)) }.sorted(by: { $0.value > $1.value }) + + let filteredLanguages = hypotheses.filter { supportedTranslationLanguages.contains(normalizeTranslationLanguage($0.key.rawValue)) }.sorted(by: { $0.value > $1.value }) if let language = filteredLanguages.first { - let languageCode = normalize(language.key.rawValue) + let languageCode = normalizeTranslationLanguage(language.key.rawValue) return (!dontTranslateLanguages.contains(languageCode), languageCode) } else { return (false, nil) diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift index db02dfa65d..776763d2b5 100644 --- a/submodules/TranslateUI/Sources/TranslateScreen.swift +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -1022,11 +1022,7 @@ public class TranslateScreen: ViewController { } } - if toLanguage == "nb" { - toLanguage = "no" - } else if toLanguage == "pt-br" { - toLanguage = "pt" - } + toLanguage = normalizeTranslationLanguage(toLanguage) var copyTranslationImpl: ((String) -> Void)? var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)? diff --git a/submodules/UndoUI/BUILD b/submodules/UndoUI/BUILD index 059920aa56..66933e09f8 100644 --- a/submodules/UndoUI/BUILD +++ b/submodules/UndoUI/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/PhotoResources", ], visibility = [ "//visibility:public", diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 3aa34a3405..ca2d9acc38 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -45,9 +45,11 @@ public enum UndoOverlayContent { case image(image: UIImage, title: String?, text: String, round: Bool, undoText: String?) case notificationSoundAdded(title: String, text: String, action: (() -> Void)?) case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?, timeout: Double?) + case universalImage(image: UIImage, size: CGSize?, title: String?, text: String, customUndoText: String?, timeout: Double?) case premiumPaywall(title: String?, text: String, customUndoText: String?, timeout: Double?, linkAction: ((String) -> Void)?) case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?) case messageTagged(context: AccountContext, isSingleMessage: Bool, customEmoji: TelegramMediaFile, isBuiltinReaction: Bool, customUndoText: String?) + case media(context: AccountContext, file: FileMediaReference, title: String?, text: String, undoText: String?, customAction: (() -> Void)?) } public enum UndoOverlayAction { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 4705c4afc6..d04b022b4d 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -23,6 +23,7 @@ import TextNodeWithEntities import BundleIconComponent import AnimatedTextComponent import ComponentDisplayAdapters +import PhotoResources final class UndoOverlayControllerNode: ViewControllerTracingNode { private let presentationData: PresentationData @@ -42,6 +43,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { private var slotMachineNode: SlotMachineAnimationNode? private var stillStickerNode: TransformImageNode? private var stickerImageSize: CGSize? + private var stickerSourceSize: CGSize? private var stickerOffset: CGPoint? private var emojiStatus: ComponentView? private let titleNode: ImmediateTextNode @@ -1066,6 +1068,56 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 5 + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } + case let .universalImage(image, size, title, text, customUndoText, timeout): + self.iconNode = ASImageNode() + self.iconNode?.displayWithoutProcessing = true + self.iconNode?.displaysAsynchronously = false + self.iconNode?.image = image + self.iconImageSize = size + + self.avatarNode = nil + self.iconCheckNode = nil + self.animationNode = nil + self.animatedStickerNode = nil + + if let title = title, text.isEmpty { + self.titleNode.attributedText = nil + let body = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(title, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + } else { + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + } + + if text.contains("](") { + isUserInteractionEnabled = true + } + self.originalRemainingSeconds = timeout ?? (isUserInteractionEnabled ? 5 : 3) + + self.textNode.maximumNumberOfLines = 5 + if let customUndoText = customUndoText { undoText = customUndoText displayUndo = true @@ -1247,6 +1299,58 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { displayUndo = false } + case let .media(context, file, title, text, customUndoText, _): + self.avatarNode = nil + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = nil + + let stillStickerNode = TransformImageNode() + + self.stillStickerNode = stillStickerNode + + var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedFetchSignal: Signal? + + updatedImageSignal = mediaGridMessageVideo(postbox: context.account.postbox, userLocation: .other, videoReference: file, onlyFullSize: false, useLargeThumbnail: false, autoFetchFullSizeThumbnail: false) + updatedFetchSignal = nil + self.stickerImageSize = CGSize(width: 30.0, height: 30.0) + self.stickerSourceSize = file.media.dimensions?.cgSize + + if let title = title { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + } else { + self.titleNode.attributedText = nil + } + + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in + return ("URL", contents) + }), textAlignment: .natural) + self.textNode.attributedText = attributedText + self.textNode.maximumNumberOfLines = 5 + + if text.contains("](") { + isUserInteractionEnabled = true + } + + if let customUndoText = customUndoText { + undoText = customUndoText + displayUndo = true + } else { + displayUndo = false + } + self.originalRemainingSeconds = isUserInteractionEnabled ? 5 : 3 + + if let updatedFetchSignal = updatedFetchSignal { + self.fetchResourceDisposable = updatedFetchSignal.start() + } + + if let updatedImageSignal = updatedImageSignal { + stillStickerNode.setSignal(updatedImageSignal) + } } self.remainingSeconds = self.originalRemainingSeconds @@ -1284,13 +1388,13 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { self.isUserInteractionEnabled = false } - case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .premiumPaywall, .peers, .messageTagged: + case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal,. universalImage, .premiumPaywall, .peers, .messageTagged: if self.textNode.tapAttributeAction != nil || displayUndo { self.isUserInteractionEnabled = true } else { self.isUserInteractionEnabled = false } - case .sticker, .customEmoji: + case .sticker, .customEmoji, .media: self.isUserInteractionEnabled = displayUndo case .dice: self.panelWrapperNode.clipsToBounds = true @@ -1418,6 +1522,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { let _ = self.action(.undo) } + case let .media(_, _, _, _, _, customAction): + if let customAction = customAction { + customAction() + } else { + let _ = self.action(.undo) + } default: let _ = self.action(.undo) } @@ -1725,7 +1835,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if let stillStickerNode = self.stillStickerNode { let makeImageLayout = stillStickerNode.asyncLayout() - let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: stickerImageSize, boundingSize: stickerImageSize, intrinsicInsets: UIEdgeInsets())) + + var radius: CGFloat = 0.0 + if case .media = self.content { + radius = 6.0 + } + var stickerImageSourceSize = stickerImageSize + if let stickerSourceSize = self.stickerSourceSize { + stickerImageSourceSize = stickerSourceSize.aspectFilled(stickerImageSourceSize) + } + let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: stickerImageSourceSize, boundingSize: stickerImageSize, intrinsicInsets: UIEdgeInsets())) let _ = imageApply() transition.updateFrame(node: stillStickerNode, frame: iconFrame) } diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index a07d619330..d13355b17b 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -697,23 +697,6 @@ public func patternColor(for color: UIColor, intensity: CGFloat, prominent: Bool return .black } -public func solidColorImage(_ color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return .single({ arguments in - guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { - return nil - } - - context.withFlippedContext { c in - c.setFillColor(color.withAlphaComponent(1.0).cgColor) - c.fill(arguments.drawingRect) - } - - addCorners(context, arguments: arguments) - - return context - }) -} - public func drawWallpaperGradientImage(_ colors: [UIColor], rotation: Int32? = nil, context: CGContext, size: CGSize) { guard !colors.isEmpty else { return diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 9ee319ed4f..b1ac4f8d1f 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -452,7 +452,7 @@ public final class WebAppController: ViewController, AttachmentContainable { if let url = navigationAction.request.url?.absoluteString { if isTelegramMeLink(url) || isTelegraPhLink(url) { decisionHandler(.cancel) - self.controller?.openUrl(url, true, {}) + self.controller?.openUrl(url, true, false, {}) } else { decisionHandler(.allow) } @@ -463,7 +463,7 @@ public final class WebAppController: ViewController, AttachmentContainable { func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { if navigationAction.targetFrame == nil, let url = navigationAction.request.url { - self.controller?.openUrl(url.absoluteString, true, {}) + self.controller?.openUrl(url.absoluteString, true, false, {}) } return nil } @@ -587,6 +587,8 @@ public final class WebAppController: ViewController, AttachmentContainable { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } + + private var updateWebViewWhenStable = false func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previousLayout = self.validLayout?.0 @@ -604,7 +606,10 @@ public final class WebAppController: ViewController, AttachmentContainable { scrollInset.bottom = 0.0 } - let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset))) + let frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset))) + if !webView.frame.width.isZero && webView.frame != frame { + self.updateWebViewWhenStable = true + } var bottomInset = layout.intrinsicInsets.bottom + layout.additionalInsets.bottom if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 { @@ -635,9 +640,18 @@ public final class WebAppController: ViewController, AttachmentContainable { if let controller = self.controller { webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition) + if self.updateWebViewWhenStable && !controller.isContainerPanning() { + self.updateWebViewWhenStable = false + webView.setNeedsLayout() + } } - webView.customBottomInset = layout.intrinsicInsets.bottom + if layout.intrinsicInsets.bottom > 44.0 { + webView.customBottomInset = 0.0 + } else { + webView.customBottomInset = layout.intrinsicInsets.bottom + } + webView.customSideInset = layout.safeInsets.left } if let placeholderNode = self.placeholderNode { @@ -788,7 +802,8 @@ public final class WebAppController: ViewController, AttachmentContainable { controller.dismiss() case "web_app_open_tg_link": if let json = json, let path = json["path_full"] as? String { - controller.openUrl("https://t.me\(path)", false, { [weak controller] in + let forceRequest = json["force_request"] as? Bool ?? false + controller.openUrl("https://t.me\(path)", false, forceRequest, { [weak controller] in let _ = controller // controller?.dismiss() }) @@ -1874,7 +1889,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private var hasSettings = false - public var openUrl: (String, Bool, @escaping () -> Void) -> Void = { _, _, _ in } + public var openUrl: (String, Bool, Bool, @escaping () -> Void) -> Void = { _, _, _, _ in } public var getNavigationController: () -> NavigationController? = { return nil } public var completion: () -> Void = {} public var requestSwitchInline: (String, [ReplyMarkupButtonRequestPeerType]?, @escaping () -> Void) -> Void = { _, _, _ in } @@ -2126,7 +2141,7 @@ public final class WebAppController: ViewController, AttachmentContainable { let context = self.context let _ = (cachedWebAppTermsPage(context: context) |> deliverOnMainQueue).startStandalone(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: true, openPeer: { peer, navigation in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: true, forceUpdate: false, openPeer: { peer, navigation in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak self] c, arguments in self?.push(c) }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) @@ -2355,7 +2370,7 @@ public func standaloneWebAppController( updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, params: WebAppParameters, threadId: Int64?, - openUrl: @escaping (String, Bool, @escaping () -> Void) -> Void, + openUrl: @escaping (String, Bool, Bool, @escaping () -> Void) -> Void, requestSwitchInline: @escaping (String, [ReplyMarkupButtonRequestPeerType]?, @escaping () -> Void) -> Void = { _, _, _ in }, getInputContainerNode: @escaping () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }, completion: @escaping () -> Void = {}, diff --git a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift index 534d977642..263e8c9b01 100644 --- a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift +++ b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift @@ -3,6 +3,7 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display +import ComponentFlow import Postbox import TelegramCore import TelegramPresentationData @@ -12,6 +13,7 @@ import AppBundle import AvatarNode import CheckNode import Markdown +import EmojiStatusComponent private let textFont = Font.regular(13.0) private let boldTextFont = Font.semibold(13.0) @@ -21,6 +23,8 @@ private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, t } private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { + private let context: AccountContext + private let presentationTheme: PresentationTheme private let strings: PresentationStrings private let peer: EnginePeer private let title: String @@ -28,6 +32,7 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { private let showMore: Bool private let titleNode: ImmediateTextNode + private var titleCredibilityIconView: ComponentHostView? private let textNode: ASTextNode private let avatarNode: AvatarNode @@ -57,7 +62,9 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { } init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, title: String, text: String, showMore: Bool, requestWriteAccess: Bool, actions: [TextAlertAction], morePressed: @escaping () -> Void, termsPressed: @escaping () -> Void) { + self.context = context self.strings = strings + self.presentationTheme = ptheme self.peer = peer self.title = title self.text = text @@ -209,7 +216,42 @@ private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode { } let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + var totalWidth = titleSize.width + + if self.peer.isVerified { + let statusContent: EmojiStatusComponent.Content = .verified(fillColor: self.presentationTheme.list.itemCheckColors.fillColor, foregroundColor: self.presentationTheme.list.itemCheckColors.foregroundColor, sizeType: .large) + let titleCredibilityIconTransition: ComponentTransition = .immediate + + let titleCredibilityIconView: ComponentHostView + if let current = self.titleCredibilityIconView { + titleCredibilityIconView = current + } else { + titleCredibilityIconView = ComponentHostView() + self.titleCredibilityIconView = titleCredibilityIconView + self.view.addSubview(titleCredibilityIconView) + } + + let titleIconSize = titleCredibilityIconView.update( + transition: titleCredibilityIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + content: statusContent, + isVisibleForAnimations: true, + action: { + } + )), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + + totalWidth += titleIconSize.width + 2.0 + + titleCredibilityIconTransition.setFrame(view: titleCredibilityIconView, frame: CGRect(origin: CGPoint(x:floorToScreenPixels((size.width - totalWidth) / 2.0) + titleSize.width + 2.0, y: origin.y), size: titleIconSize)) + } + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: origin.y), size: titleSize)) origin.y += titleSize.height + 6.0 var entriesHeight: CGFloat = 0.0 diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index ce602c1b18..20956490af 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -48,7 +48,7 @@ private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(in " style.appendChild(document.createTextNode(css)); head.appendChild(style);" private let videoSource = """ -function disableWebkitEnterFullscreen(videoElement) { +function tgBrowserDisableWebkitEnterFullscreen(videoElement) { if (videoElement && videoElement.webkitEnterFullscreen) { Object.defineProperty(videoElement, 'webkitEnterFullscreen', { value: undefined @@ -56,11 +56,11 @@ function disableWebkitEnterFullscreen(videoElement) { } } -function disableFullscreenOnExistingVideos() { - document.querySelectorAll('video').forEach(disableWebkitEnterFullscreen); +function tgBrowserDisableFullscreenOnExistingVideos() { + document.querySelectorAll('video').forEach(tgBrowserDisableWebkitEnterFullscreen); } -function handleMutations(mutations) { +function tgBrowserHandleMutations(mutations) { mutations.forEach((mutation) => { if (mutation.addedNodes && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((newNode) => { @@ -75,22 +75,30 @@ function handleMutations(mutations) { }); } -disableFullscreenOnExistingVideos(); +tgBrowserDisableFullscreenOnExistingVideos(); -const observer = new MutationObserver(handleMutations); +const _tgbrowser_observer = new MutationObserver(tgBrowserHandleMutations); -observer.observe(document.body, { +_tgbrowser_observer.observe(document.body, { childList: true, subtree: true }); -function disconnectObserver() { - observer.disconnect(); +function tgBrowserDisconnectObserver() { + _tgbrowser_observer.disconnect(); } """ final class WebAppWebView: WKWebView { var handleScriptMessage: (WKScriptMessage) -> Void = { _ in } + + var customSideInset: CGFloat = 0.0 { + didSet { + if self.customSideInset != oldValue { + self.setNeedsLayout() + } + } + } var customBottomInset: CGFloat = 0.0 { didSet { @@ -101,7 +109,7 @@ final class WebAppWebView: WKWebView { } override var safeAreaInsets: UIEdgeInsets { - return UIEdgeInsets(top: 0.0, left: 0.0, bottom: self.customBottomInset, right: 0.0) + return UIEdgeInsets(top: 0.0, left: self.customSideInset, bottom: self.customBottomInset, right: self.customSideInset) } init(account: Account) { diff --git a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh index 0bd88402a1..31bfa57c0a 100755 --- a/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh +++ b/submodules/ffmpeg/Sources/FFMpeg/build-ffmpeg-bazel.sh @@ -48,7 +48,7 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ --enable-libvpx \ --enable-audiotoolbox \ --enable-bsf=aac_adtstoasc,vp9_superframe,h264_mp4toannexb \ - --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,pcm_f32le,gsm_ms_at \ + --enable-decoder=h264,libvpx_vp9,hevc,libopus,mp3,aac,flac,alac_at,pcm_s16le,pcm_s24le,pcm_f32le,gsm_ms_at,vorbis \ --enable-encoder=libvpx_vp9,aac_at \ --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska,mpegts \ --enable-parser=aac,h264,mp3,libopus \ diff --git a/versions.json b/versions.json index 3556a03d85..1a573248dd 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.2.1", + "app": "11.3", "xcode": "16.0", "bazel": "7.3.1", "macos": "15.0"